Compare commits
91 commits
9d813802a9
...
96f1a9d9f2
Author | SHA1 | Date | |
---|---|---|---|
Natsu Kagami | 96f1a9d9f2 | ||
Natsu Kagami | 88901aa070 | ||
32b72f9297 | |||
57dead7960 | |||
9786752a4f | |||
ed8c9e994b | |||
8cf30773ce | |||
6540dd5642 | |||
c80c8b3294 | |||
e1ae89b00e | |||
f9299ac15c | |||
df9eeeb0b3 | |||
32bf258bbf | |||
f56a44ac97 | |||
0a7f158b70 | |||
ab1b34d4d2 | |||
f2f7b7fe1f | |||
7264f543bd | |||
66e4ba4991 | |||
f6864f96bd | |||
f67d4fd916 | |||
cd403fe605 | |||
5481aa12be | |||
806ad2c6a2 | |||
d1b8d737cc | |||
a095a30500 | |||
5de7eec2ca | |||
b8767f3618 | |||
68759e64d1 | |||
78a6f13380 | |||
a697fb04df | |||
39f7d4e00d | |||
12d0e6aed8 | |||
769a5cb099 | |||
d6d10d091e | |||
5c6e9756d0 | |||
eace6c4d9b | |||
4723358d2d | |||
aad855cafc | |||
643b6bce07 | |||
5faf911b17 | |||
ddd1ec5819 | |||
8cd3e38f22 | |||
be964f933c | |||
d429ef9161 | |||
9885c8f388 | |||
8be2c738df | |||
faa7ffc310 | |||
4ac2e4aa7b | |||
60d55d45c2 | |||
4436c337dd | |||
c335655896 | |||
48f1527cc6 | |||
fcbf99f121 | |||
028b30a334 | |||
5793476223 | |||
715357c8c9 | |||
56365ebc39 | |||
a1a78370cc | |||
7e993704cc | |||
f05267b216 | |||
634e81e9d0 | |||
52c63690a3 | |||
348efe0069 | |||
9f6236762d | |||
8a4ab1bdb9 | |||
a32a264159 | |||
a364488895 | |||
d05f0a4f23 | |||
49fdcf7837 | |||
baa2605d27 | |||
359fd92ae0 | |||
6a16b25722 | |||
4dd706ff96 | |||
30f6d50a68 | |||
3042dea886 | |||
ac14e61b6d | |||
27b0813e49 | |||
99d7525436 | |||
f9cb9502b1 | |||
01c90150a8 | |||
c1da6b8767 | |||
dc06508aa5 | |||
8c4a88b333 | |||
8a10ffd477 | |||
b6c59d4ee1 | |||
13cf7b3f92 | |||
fd1b45900d | |||
0f5edef199 | |||
4dfc0d0b41 | |||
b7416bc17d |
4
.github/workflows/prodtag.yml
vendored
4
.github/workflows/prodtag.yml
vendored
|
@ -12,5 +12,5 @@ jobs:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
ref: production
|
ref: production
|
||||||
- run: git tag -a "'{date +%Y.%m.%d}.{git rev-parse --short HEAD}'" $(git rev-parse HEAD)
|
- run: git tag "`date +%Y.%m.%d`.`git rev-parse --short HEAD`" $(git rev-parse HEAD)
|
||||||
- run: git push
|
- run: git push --tags
|
||||||
|
|
25
.github/workflows/tagrelease.yml
vendored
Normal file
25
.github/workflows/tagrelease.yml
vendored
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
name: Create Release on every tag push in `production`
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- production
|
||||||
|
tags:
|
||||||
|
- '*'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: production
|
||||||
|
- uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 18
|
||||||
|
- run: npm ci && npm run build
|
||||||
|
- run: cd dist && zip -r ../phanpy-dist.zip . && cd ..
|
||||||
|
- uses: softprops/action-gh-release@v1
|
||||||
|
with:
|
||||||
|
generate_release_notes: true
|
||||||
|
files: phanpy-dist.zip
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -26,6 +26,7 @@ dist-ssr
|
||||||
# Custom
|
# Custom
|
||||||
.env.dev
|
.env.dev
|
||||||
src/data/instances-full.json
|
src/data/instances-full.json
|
||||||
|
phanpy-dist.zip
|
||||||
|
|
||||||
# Nix
|
# Nix
|
||||||
.direnv
|
.direnv
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
"index.css$",
|
"index.css$",
|
||||||
".css$",
|
".css$",
|
||||||
"<THIRD_PARTY_MODULES>",
|
"<THIRD_PARTY_MODULES>",
|
||||||
|
"/assets/",
|
||||||
"^../",
|
"^../",
|
||||||
"^[./]"
|
"^[./]"
|
||||||
],
|
],
|
||||||
|
|
|
@ -121,6 +121,7 @@ Try search for "how to self-host static sites" as there are many ways to do it.
|
||||||
- [React Router](https://reactrouter.com/) - Routing
|
- [React Router](https://reactrouter.com/) - Routing
|
||||||
- [masto.js](https://github.com/neet/masto.js/) - Mastodon API client
|
- [masto.js](https://github.com/neet/masto.js/) - Mastodon API client
|
||||||
- [Iconify](https://iconify.design/) - Icon library
|
- [Iconify](https://iconify.design/) - Icon library
|
||||||
|
- [MingCute icons](https://www.mingcute.com/)
|
||||||
- Vanilla CSS - *Yes, I'm old school.*
|
- Vanilla CSS - *Yes, I'm old school.*
|
||||||
|
|
||||||
Some of these may change in the future. The front-end world is ever-changing.
|
Some of these may change in the future. The front-end world is ever-changing.
|
||||||
|
|
14
flake.lock
14
flake.lock
|
@ -5,11 +5,11 @@
|
||||||
"systems": "systems"
|
"systems": "systems"
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1689068808,
|
"lastModified": 1694529238,
|
||||||
"narHash": "sha256-6ixXo3wt24N/melDWjq70UuHQLxGV8jZvooRanIHXw0=",
|
"narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=",
|
||||||
"owner": "numtide",
|
"owner": "numtide",
|
||||||
"repo": "flake-utils",
|
"repo": "flake-utils",
|
||||||
"rev": "919d646de7be200f3bf08cb76ae1f09402b6f9b4",
|
"rev": "ff7b65b44d01cf9ba6a71320833626af21126384",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
@ -20,16 +20,16 @@
|
||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1689413807,
|
"lastModified": 1697059129,
|
||||||
"narHash": "sha256-exuzOvOhGAEKWQKwDuZAL4N8a1I837hH5eocaTcIbLc=",
|
"narHash": "sha256-9NJcFF9CEYPvHJ5ckE8kvINvI84SZZ87PvqMbH6pro0=",
|
||||||
"owner": "nixOS",
|
"owner": "nixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "46ed466081b9cad1125b11f11a2af5cc40b942c7",
|
"rev": "5e4c2ada4fcd54b99d56d7bd62f384511a7e2593",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"owner": "nixOS",
|
"owner": "nixOS",
|
||||||
"ref": "nixpkgs-unstable",
|
"ref": "nixos-unstable",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
inputs.nixpkgs.url = github:nixOS/nixpkgs/nixpkgs-unstable;
|
inputs.nixpkgs.url = github:nixOS/nixpkgs/nixos-unstable;
|
||||||
inputs.flake-utils.url = github:numtide/flake-utils;
|
inputs.flake-utils.url = github:numtide/flake-utils;
|
||||||
|
|
||||||
outputs = { self, nixpkgs, flake-utils, ... }: flake-utils.lib.eachDefaultSystem (system:
|
outputs = { self, nixpkgs, flake-utils, ... }: flake-utils.lib.eachDefaultSystem (system:
|
||||||
|
@ -7,7 +7,7 @@
|
||||||
pkgs = import nixpkgs { inherit system; };
|
pkgs = import nixpkgs { inherit system; };
|
||||||
lib = pkgs.lib;
|
lib = pkgs.lib;
|
||||||
in
|
in
|
||||||
{
|
rec {
|
||||||
packages.default = pkgs.buildNpmPackage {
|
packages.default = pkgs.buildNpmPackage {
|
||||||
pname = "dtth-phanpy";
|
pname = "dtth-phanpy";
|
||||||
version = "0.1.0";
|
version = "0.1.0";
|
||||||
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
src = lib.cleanSource ./.;
|
src = lib.cleanSource ./.;
|
||||||
|
|
||||||
npmDepsHash = "sha256-tqR3YQ++nJmwDNKIm7uFLhJ5HlAqfeEmJVyynHx3Hzw=";
|
npmDepsHash = "sha256-LpvZfIzIdgxXg4upcDKm7jbK7CjrRvg//HULO4GDTdU=";
|
||||||
# npmDepsHash = lib.fakeHash;
|
# npmDepsHash = lib.fakeHash;
|
||||||
|
|
||||||
# DTTH-specific env variables
|
# DTTH-specific env variables
|
||||||
|
@ -33,6 +33,7 @@
|
||||||
};
|
};
|
||||||
|
|
||||||
devShells.default = pkgs.mkShell {
|
devShells.default = pkgs.mkShell {
|
||||||
|
inputsFrom = [ packages.default ];
|
||||||
buildInputs = with pkgs; [ nodejs ];
|
buildInputs = with pkgs; [ nodejs ];
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
2326
package-lock.json
generated
2326
package-lock.json
generated
File diff suppressed because it is too large
Load diff
26
package.json
26
package.json
|
@ -12,25 +12,25 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@formatjs/intl-localematcher": "~0.4.2",
|
"@formatjs/intl-localematcher": "~0.4.2",
|
||||||
"@github/text-expander-element": "~2.5.0",
|
"@github/text-expander-element": "~2.5.0",
|
||||||
"@iconify-icons/mingcute": "~1.2.7",
|
"@iconify-icons/mingcute": "~1.2.8",
|
||||||
"@justinribeiro/lite-youtube": "~1.5.0",
|
"@justinribeiro/lite-youtube": "~1.5.0",
|
||||||
"@szhsin/react-menu": "~4.0.3",
|
"@szhsin/react-menu": "~4.1.0",
|
||||||
"@uidotdev/usehooks": "~2.2.0",
|
"@uidotdev/usehooks": "~2.4.0",
|
||||||
"dayjs": "~1.11.9",
|
"dayjs": "~1.11.10",
|
||||||
"dayjs-twitter": "~0.5.0",
|
"dayjs-twitter": "~0.5.0",
|
||||||
"fast-blurhash": "~1.1.2",
|
"fast-blurhash": "~1.1.2",
|
||||||
"fast-deep-equal": "~3.1.3",
|
"fast-deep-equal": "~3.1.3",
|
||||||
"idb-keyval": "~6.2.1",
|
"idb-keyval": "~6.2.1",
|
||||||
"just-debounce-it": "~3.2.0",
|
"just-debounce-it": "~3.2.0",
|
||||||
"lz-string": "~1.5.0",
|
"lz-string": "~1.5.0",
|
||||||
"masto": "~5.11.4",
|
"masto": "~6.3.1",
|
||||||
"mem": "~9.0.2",
|
"moize": "~6.1.6",
|
||||||
"p-retry": "~6.0.0",
|
"p-retry": "~6.1.0",
|
||||||
"p-throttle": "~5.1.0",
|
"p-throttle": "~5.1.0",
|
||||||
"preact": "~10.17.1",
|
"preact": "~10.18.1",
|
||||||
"react-hotkeys-hook": "~4.4.1",
|
"react-hotkeys-hook": "~4.4.1",
|
||||||
"react-intersection-observer": "~9.5.2",
|
"react-intersection-observer": "~9.5.2",
|
||||||
"react-quick-pinch-zoom": "~4.9.0",
|
"react-quick-pinch-zoom": "~5.0.0",
|
||||||
"react-router-dom": "6.6.2",
|
"react-router-dom": "6.6.2",
|
||||||
"string-length": "5.0.1",
|
"string-length": "5.0.1",
|
||||||
"swiped-events": "~1.1.7",
|
"swiped-events": "~1.1.7",
|
||||||
|
@ -42,13 +42,13 @@
|
||||||
"valtio": "1.9.0"
|
"valtio": "1.9.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@preact/preset-vite": "~2.5.0",
|
"@preact/preset-vite": "~2.6.0",
|
||||||
"@trivago/prettier-plugin-sort-imports": "~4.2.0",
|
"@trivago/prettier-plugin-sort-imports": "~4.2.0",
|
||||||
"postcss": "~8.4.29",
|
"postcss": "~8.4.31",
|
||||||
"postcss-dark-theme-class": "~1.0.0",
|
"postcss-dark-theme-class": "~1.0.0",
|
||||||
"postcss-preset-env": "~9.1.3",
|
"postcss-preset-env": "~9.2.0",
|
||||||
"twitter-text": "~3.1.0",
|
"twitter-text": "~3.1.0",
|
||||||
"vite": "~4.4.9",
|
"vite": "~4.4.11",
|
||||||
"vite-plugin-generate-file": "~0.0.4",
|
"vite-plugin-generate-file": "~0.0.4",
|
||||||
"vite-plugin-html-config": "~1.0.11",
|
"vite-plugin-html-config": "~1.0.11",
|
||||||
"vite-plugin-pwa": "~0.16.5",
|
"vite-plugin-pwa": "~0.16.5",
|
||||||
|
|
74
public/sw.js
74
public/sw.js
|
@ -161,52 +161,40 @@ self.addEventListener('notificationclick', (event) => {
|
||||||
console.log('NOTIFICATION CLICK payload', payload);
|
console.log('NOTIFICATION CLICK payload', payload);
|
||||||
const { badge, body, data, dir, icon, lang, tag, timestamp, title } = payload;
|
const { badge, body, data, dir, icon, lang, tag, timestamp, title } = payload;
|
||||||
const { access_token, notification_type } = data;
|
const { access_token, notification_type } = data;
|
||||||
const actions = new Promise((resolve) => {
|
const url = `/#/notifications?id=${tag}&access_token=${btoa(access_token)}`;
|
||||||
event.notification.close();
|
|
||||||
const url = `/#/notifications?id=${tag}&access_token=${btoa(access_token)}`;
|
event.notification.close();
|
||||||
self.clients
|
event.waitUntil(
|
||||||
.matchAll({
|
(async () => {
|
||||||
|
const clients = await self.clients.matchAll({
|
||||||
type: 'window',
|
type: 'window',
|
||||||
includeUncontrolled: true,
|
includeUncontrolled: true,
|
||||||
})
|
});
|
||||||
.then((clients) => {
|
console.log('NOTIFICATION CLICK clients 1', clients);
|
||||||
console.log('NOTIFICATION CLICK clients 1', clients);
|
if (clients.length && 'navigate' in clients[0]) {
|
||||||
if (clients.length && 'navigate' in clients[0]) {
|
console.log('NOTIFICATION CLICK clients 2', clients);
|
||||||
console.log('NOTIFICATION CLICK clients 2', clients);
|
const bestClient =
|
||||||
const bestClient =
|
clients.find(
|
||||||
clients.find(
|
(client) => client.focused || client.visibilityState === 'visible',
|
||||||
(client) =>
|
) || clients[0];
|
||||||
client.focused || client.visibilityState === 'visible',
|
console.log('NOTIFICATION CLICK navigate', url);
|
||||||
) || clients[0];
|
if (bestClient) {
|
||||||
console.log('NOTIFICATION CLICK navigate', url);
|
console.log('NOTIFICATION CLICK postMessage', bestClient);
|
||||||
// Check if URL is root / or /notifications
|
bestClient.postMessage?.({
|
||||||
// const clientURL = new URL(bestClient.url);
|
type: 'notification',
|
||||||
// if (
|
id: tag,
|
||||||
// /^#\/?$/.test(clientURL.hash) ||
|
accessToken: access_token,
|
||||||
// /^#\/notifications/i.test(clientURL.hash)
|
});
|
||||||
// ) {
|
bestClient.focus();
|
||||||
// bestClient.navigate(url).then((client) => client?.focus());
|
|
||||||
// } else {
|
|
||||||
// User might be on a different page (e.g. composing a post), so don't navigate anywhere else
|
|
||||||
if (bestClient) {
|
|
||||||
console.log('NOTIFICATION CLICK postMessage', bestClient);
|
|
||||||
bestClient.postMessage?.({
|
|
||||||
type: 'notification',
|
|
||||||
id: tag,
|
|
||||||
accessToken: access_token,
|
|
||||||
});
|
|
||||||
bestClient.focus();
|
|
||||||
} else {
|
|
||||||
console.log('NOTIFICATION CLICK openWindow', url);
|
|
||||||
self.clients.openWindow(url);
|
|
||||||
}
|
|
||||||
// }
|
|
||||||
} else {
|
} else {
|
||||||
console.log('NOTIFICATION CLICK openWindow', url);
|
console.log('NOTIFICATION CLICK openWindow', url);
|
||||||
self.clients.openWindow(url);
|
await self.clients.openWindow(url);
|
||||||
}
|
}
|
||||||
resolve();
|
// }
|
||||||
});
|
} else {
|
||||||
});
|
console.log('NOTIFICATION CLICK openWindow', url);
|
||||||
event.waitUntil(actions);
|
await self.clients.openWindow(url);
|
||||||
|
}
|
||||||
|
})(),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
31
src/app.css
31
src/app.css
|
@ -1087,6 +1087,7 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
|
||||||
top: env(safe-area-inset-top, 0);
|
top: env(safe-area-inset-top, 0);
|
||||||
}
|
}
|
||||||
:is(.carousel-top-controls, .carousel-controls) {
|
:is(.carousel-top-controls, .carousel-controls) {
|
||||||
|
/* mix-blend-mode: luminosity; */
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0;
|
left: 0;
|
||||||
left: env(safe-area-inset-left, 0);
|
left: env(safe-area-inset-left, 0);
|
||||||
|
@ -1119,11 +1120,10 @@ button.carousel-dot {
|
||||||
button.carousel-dot {
|
button.carousel-dot {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
.carousel-controls :is(.button, button).carousel-button {
|
:is(.button, button).carousel-button {
|
||||||
background-color: var(--bg-blur-color);
|
background-color: var(--bg-blur-color);
|
||||||
}
|
}
|
||||||
.carousel-controls
|
:is(.button, button).carousel-button:is(:hover, :focus):not(:active) {
|
||||||
:is(.button, button).carousel-button:is(:hover, :focus):not(:active) {
|
|
||||||
background-color: var(--bg-color);
|
background-color: var(--bg-color);
|
||||||
}
|
}
|
||||||
.carousel-top-controls .szh-menu-container {
|
.carousel-top-controls .szh-menu-container {
|
||||||
|
@ -1140,15 +1140,19 @@ button.carousel-dot {
|
||||||
border: 1px solid var(--outline-color);
|
border: 1px solid var(--outline-color);
|
||||||
box-shadow: 0 4px 32px var(--drop-shadow-color);
|
box-shadow: 0 4px 32px var(--drop-shadow-color);
|
||||||
/* backdrop-filter: blur(12px) invert(0.25); */
|
/* backdrop-filter: blur(12px) invert(0.25); */
|
||||||
|
transition: background-color 0.2s ease-out;
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--bg-color);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
button.carousel-dot {
|
button.carousel-dot {
|
||||||
backdrop-filter: none !important;
|
backdrop-filter: none !important;
|
||||||
border: none;
|
border: none;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
button.carousel-dot[disabled] {
|
/* button.carousel-dot[disabled] {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
} */
|
||||||
button.carousel-dot .icon {
|
button.carousel-dot .icon {
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
transform: scale(0.5);
|
transform: scale(0.5);
|
||||||
|
@ -1331,12 +1335,16 @@ body:has(.media-modal-container + .status-deck) .media-post-link {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
.sheet-max {
|
.sheet-max {
|
||||||
width: 90vw;
|
|
||||||
width: 90dvw;
|
|
||||||
max-width: none;
|
max-width: none;
|
||||||
height: 90vh;
|
height: 90vh;
|
||||||
height: 90dvh;
|
height: 90dvh;
|
||||||
}
|
}
|
||||||
|
@media (min-width: 40em) {
|
||||||
|
.sheet {
|
||||||
|
width: 90vw;
|
||||||
|
width: 90dvw;
|
||||||
|
}
|
||||||
|
}
|
||||||
.sheet .sheet-close {
|
.sheet .sheet-close {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
|
@ -1423,6 +1431,10 @@ body:has(.media-modal-container + .status-deck) .media-post-link {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin: 4px;
|
margin: 4px;
|
||||||
align-self: center;
|
align-self: center;
|
||||||
|
|
||||||
|
&.clickable {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.tag .icon {
|
.tag .icon {
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
|
@ -1742,7 +1754,7 @@ meter.donut[hidden] {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
text-shadow: 0 1px var(--bg-color);
|
text-shadow: 0 1px var(--bg-color);
|
||||||
background-color: var(--bg-color);
|
background-color: var(--bg-color);
|
||||||
border: 1px solid var(--outline-color);
|
border: 2px solid var(--link-faded-color);
|
||||||
box-shadow: 0 3px 16px var(--drop-shadow-color),
|
box-shadow: 0 3px 16px var(--drop-shadow-color),
|
||||||
0 6px 16px -3px var(--drop-shadow-color);
|
0 6px 16px -3px var(--drop-shadow-color);
|
||||||
}
|
}
|
||||||
|
@ -1750,8 +1762,7 @@ meter.donut[hidden] {
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
border-color: var(--link-color);
|
border-color: var(--link-color);
|
||||||
filter: none !important;
|
filter: none !important;
|
||||||
box-shadow: 0 0 0 1px var(--link-text-color),
|
box-shadow: 0 3px 16px var(--drop-shadow-color),
|
||||||
0 3px 16px var(--drop-shadow-color),
|
|
||||||
0 6px 16px -3px var(--drop-shadow-color),
|
0 6px 16px -3px var(--drop-shadow-color),
|
||||||
0 6px 16px var(--drop-shadow-color);
|
0 6px 16px var(--drop-shadow-color);
|
||||||
}
|
}
|
||||||
|
|
23
src/app.jsx
23
src/app.jsx
|
@ -114,28 +114,31 @@ function App() {
|
||||||
code,
|
code,
|
||||||
});
|
});
|
||||||
|
|
||||||
const masto = initClient({ instance: instanceURL, accessToken });
|
const client = initClient({ instance: instanceURL, accessToken });
|
||||||
await Promise.allSettled([
|
await Promise.allSettled([
|
||||||
initInstance(masto, instanceURL),
|
initInstance(client, instanceURL),
|
||||||
initAccount(masto, instanceURL, accessToken, vapidKey),
|
initAccount(client, instanceURL, accessToken, vapidKey),
|
||||||
]);
|
]);
|
||||||
initStates();
|
initStates();
|
||||||
initPreferences(masto);
|
initPreferences(client);
|
||||||
|
|
||||||
setIsLoggedIn(true);
|
setIsLoggedIn(true);
|
||||||
setUIState('default');
|
setUIState('default');
|
||||||
})();
|
})();
|
||||||
} else {
|
} else {
|
||||||
|
window.__IGNORE_GET_ACCOUNT_ERROR__ = true;
|
||||||
const account = getCurrentAccount();
|
const account = getCurrentAccount();
|
||||||
if (account) {
|
if (account) {
|
||||||
store.session.set('currentAccount', account.info.id);
|
store.session.set('currentAccount', account.info.id);
|
||||||
const { masto, instance } = api({ account });
|
const { client } = api({ account });
|
||||||
console.log('masto', masto);
|
const { instance } = client;
|
||||||
initPreferences(masto);
|
// console.log('masto', masto);
|
||||||
|
initStates();
|
||||||
|
initPreferences(client);
|
||||||
setUIState('loading');
|
setUIState('loading');
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
await initInstance(masto, instance);
|
await initInstance(client, instance);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoggedIn(true);
|
setIsLoggedIn(true);
|
||||||
|
@ -251,9 +254,9 @@ function App() {
|
||||||
<Shortcuts />
|
<Shortcuts />
|
||||||
)}
|
)}
|
||||||
<Modals />
|
<Modals />
|
||||||
<NotificationService />
|
{isLoggedIn && <NotificationService />}
|
||||||
<BackgroundService isLoggedIn={isLoggedIn} />
|
<BackgroundService isLoggedIn={isLoggedIn} />
|
||||||
<SearchCommand onClose={focusDeck} />
|
{uiState !== 'loading' && <SearchCommand onClose={focusDeck} />}
|
||||||
<KeyboardShortcutsHelp />
|
<KeyboardShortcutsHelp />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -3,7 +3,6 @@ import './account-block.css';
|
||||||
// import { useNavigate } from 'react-router-dom';
|
// import { useNavigate } from 'react-router-dom';
|
||||||
import enhanceContent from '../utils/enhance-content';
|
import enhanceContent from '../utils/enhance-content';
|
||||||
import niceDateTime from '../utils/nice-date-time';
|
import niceDateTime from '../utils/nice-date-time';
|
||||||
import shortenNumber from '../utils/shorten-number';
|
|
||||||
import states from '../utils/states';
|
import states from '../utils/states';
|
||||||
|
|
||||||
import Avatar from './avatar';
|
import Avatar from './avatar';
|
||||||
|
|
|
@ -220,6 +220,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.account-container .actions {
|
.account-container .actions {
|
||||||
|
margin-block: 8px;
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
@ -342,23 +343,82 @@
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.account-container .posting-stats {
|
.account-container .posting-stats-button {
|
||||||
font-size: 90%;
|
display: flex;
|
||||||
color: var(--text-insignificant-color);
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
width: 100%;
|
||||||
|
color: inherit;
|
||||||
background-color: var(--bg-faded-color);
|
background-color: var(--bg-faded-color);
|
||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
--size: 8px;
|
font-size: 90%;
|
||||||
--original-color: var(--link-color);
|
color: var(--text-insignificant-color);
|
||||||
|
line-height: 1;
|
||||||
|
vertical-align: text-top;
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
&:is(:hover, :focus-within) {
|
&:is(:hover, :focus-within) {
|
||||||
|
color: var(--text-color);
|
||||||
background-color: var(--link-bg-hover-color);
|
background-color: var(--link-bg-hover-color);
|
||||||
|
filter: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader-container {
|
||||||
|
margin: 0;
|
||||||
|
opacity: 0.5;
|
||||||
|
transform: scale(0.75);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes wobble {
|
||||||
|
0% {
|
||||||
|
transform: rotate(-4deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: rotate(4deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes loading-spin {
|
||||||
|
0% {
|
||||||
|
transform: rotate(0deg) scale(0.75);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg) scale(0.75);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.posting-stats-icon {
|
||||||
|
display: inline-block;
|
||||||
|
width: 24px;
|
||||||
|
height: 8px;
|
||||||
|
filter: opacity(0.75);
|
||||||
|
animation: wobble 2s linear both infinite alternate !important;
|
||||||
|
|
||||||
|
&.loading {
|
||||||
|
animation: loading-spin 0.35s linear both infinite !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-container {
|
||||||
|
--posting-stats-size: 8px;
|
||||||
|
--original-color: var(--link-color);
|
||||||
|
|
||||||
|
.posting-stats {
|
||||||
|
font-size: 90%;
|
||||||
|
color: var(--text-insignificant-color);
|
||||||
|
background-color: var(--bg-faded-color);
|
||||||
|
padding: 8px 12px;
|
||||||
|
|
||||||
|
&:is(:hover, :focus-within) {
|
||||||
|
background-color: var(--link-bg-hover-color);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.posting-stats-bar {
|
.posting-stats-bar {
|
||||||
--gap: 0.5px;
|
--gap: 0.5px;
|
||||||
--gap-color: var(--outline-color);
|
--gap-color: var(--outline-color);
|
||||||
height: var(--size);
|
height: var(--posting-stats-size);
|
||||||
border-radius: var(--size);
|
border-radius: var(--posting-stats-size);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
margin: 8px 0;
|
margin: 8px 0;
|
||||||
box-shadow: inset 0 0 0 1px var(--outline-color),
|
box-shadow: inset 0 0 0 1px var(--outline-color),
|
||||||
|
@ -388,9 +448,9 @@
|
||||||
|
|
||||||
.posting-stats-legend-item {
|
.posting-stats-legend-item {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: var(--size);
|
width: var(--posting-stats-size);
|
||||||
height: var(--size);
|
height: var(--posting-stats-size);
|
||||||
border-radius: var(--size);
|
border-radius: var(--posting-stats-size);
|
||||||
background-color: var(--text-insignificant-color);
|
background-color: var(--text-insignificant-color);
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
margin: 0 4px 2px;
|
margin: 0 4px 2px;
|
||||||
|
|
|
@ -1,14 +1,21 @@
|
||||||
import './account-info.css';
|
import './account-info.css';
|
||||||
|
|
||||||
import { Menu, MenuDivider, MenuItem, SubMenu } from '@szhsin/react-menu';
|
import { Menu, MenuDivider, MenuItem, SubMenu } from '@szhsin/react-menu';
|
||||||
import { useEffect, useMemo, useReducer, useRef, useState } from 'preact/hooks';
|
import {
|
||||||
import { proxy, useSnapshot } from 'valtio';
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useReducer,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'preact/hooks';
|
||||||
|
|
||||||
import { api } from '../utils/api';
|
import { api } from '../utils/api';
|
||||||
import enhanceContent from '../utils/enhance-content';
|
import enhanceContent from '../utils/enhance-content';
|
||||||
import getHTMLText from '../utils/getHTMLText';
|
import getHTMLText from '../utils/getHTMLText';
|
||||||
import handleContentLinks from '../utils/handle-content-links';
|
import handleContentLinks from '../utils/handle-content-links';
|
||||||
import niceDateTime from '../utils/nice-date-time';
|
import niceDateTime from '../utils/nice-date-time';
|
||||||
|
import pmem from '../utils/pmem';
|
||||||
import shortenNumber from '../utils/shorten-number';
|
import shortenNumber from '../utils/shorten-number';
|
||||||
import showToast from '../utils/show-toast';
|
import showToast from '../utils/show-toast';
|
||||||
import states, { hideAllModals } from '../utils/states';
|
import states, { hideAllModals } from '../utils/states';
|
||||||
|
@ -49,8 +56,64 @@ const MUTE_DURATIONS_LABELS = {
|
||||||
|
|
||||||
const LIMIT = 80;
|
const LIMIT = 80;
|
||||||
|
|
||||||
const accountInfoStates = proxy({
|
const ACCOUNT_INFO_MAX_AGE = 1000 * 60 * 10; // 10 mins
|
||||||
familiarFollowers: [],
|
|
||||||
|
function fetchFamiliarFollowers(currentID, masto) {
|
||||||
|
return masto.v1.accounts.familiarFollowers.fetch({
|
||||||
|
id: [currentID],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const memFetchFamiliarFollowers = pmem(fetchFamiliarFollowers, {
|
||||||
|
maxAge: ACCOUNT_INFO_MAX_AGE,
|
||||||
|
});
|
||||||
|
|
||||||
|
async function fetchPostingStats(accountID, masto) {
|
||||||
|
const fetchStatuses = masto.v1.accounts
|
||||||
|
.$select(accountID)
|
||||||
|
.statuses.list({
|
||||||
|
limit: 20,
|
||||||
|
})
|
||||||
|
.next();
|
||||||
|
|
||||||
|
const { value: statuses } = await fetchStatuses;
|
||||||
|
console.log('fetched statuses', statuses);
|
||||||
|
const stats = {
|
||||||
|
total: statuses.length,
|
||||||
|
originals: 0,
|
||||||
|
replies: 0,
|
||||||
|
boosts: 0,
|
||||||
|
};
|
||||||
|
// Categories statuses by type
|
||||||
|
// - Original posts (not replies to others)
|
||||||
|
// - Threads (self-replies + 1st original post)
|
||||||
|
// - Boosts (reblogs)
|
||||||
|
// - Replies (not-self replies)
|
||||||
|
statuses.forEach((status) => {
|
||||||
|
if (status.reblog) {
|
||||||
|
stats.boosts++;
|
||||||
|
} else if (
|
||||||
|
!!status.inReplyToId &&
|
||||||
|
status.inReplyToAccountId !== status.account.id // Not self-reply
|
||||||
|
) {
|
||||||
|
stats.replies++;
|
||||||
|
} else {
|
||||||
|
stats.originals++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Count days since last post
|
||||||
|
if (statuses.length) {
|
||||||
|
stats.daysSinceLastPost = Math.ceil(
|
||||||
|
(Date.now() - new Date(statuses[statuses.length - 1].createdAt)) /
|
||||||
|
86400000,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('posting stats', stats);
|
||||||
|
return stats;
|
||||||
|
}
|
||||||
|
const memFetchPostingStats = pmem(fetchPostingStats, {
|
||||||
|
maxAge: ACCOUNT_INFO_MAX_AGE,
|
||||||
});
|
});
|
||||||
|
|
||||||
function AccountInfo({
|
function AccountInfo({
|
||||||
|
@ -63,10 +126,10 @@ function AccountInfo({
|
||||||
const { masto } = api({
|
const { masto } = api({
|
||||||
instance,
|
instance,
|
||||||
});
|
});
|
||||||
|
const { masto: currentMasto } = api();
|
||||||
const [uiState, setUIState] = useState('default');
|
const [uiState, setUIState] = useState('default');
|
||||||
const isString = typeof account === 'string';
|
const isString = typeof account === 'string';
|
||||||
const [info, setInfo] = useState(isString ? null : account);
|
const [info, setInfo] = useState(isString ? null : account);
|
||||||
const snapAccountInfoStates = useSnapshot(accountInfoStates);
|
|
||||||
|
|
||||||
const isSelf = useMemo(
|
const isSelf = useMemo(
|
||||||
() => account.id === store.session.get('currentAccount'),
|
() => account.id === store.session.get('currentAccount'),
|
||||||
|
@ -121,6 +184,7 @@ function AccountInfo({
|
||||||
username,
|
username,
|
||||||
memorial,
|
memorial,
|
||||||
moved,
|
moved,
|
||||||
|
roles,
|
||||||
} = info || {};
|
} = info || {};
|
||||||
let headerIsAvatar = false;
|
let headerIsAvatar = false;
|
||||||
let { header, headerStatic } = info || {};
|
let { header, headerStatic } = info || {};
|
||||||
|
@ -134,13 +198,19 @@ function AccountInfo({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const accountInstance = useMemo(() => {
|
||||||
|
if (!url) return null;
|
||||||
|
const domain = new URL(url).hostname;
|
||||||
|
return domain;
|
||||||
|
}, [url]);
|
||||||
|
|
||||||
const [headerCornerColors, setHeaderCornerColors] = useState([]);
|
const [headerCornerColors, setHeaderCornerColors] = useState([]);
|
||||||
|
|
||||||
const followersIterator = useRef();
|
const followersIterator = useRef();
|
||||||
const familiarFollowersCache = useRef([]);
|
const familiarFollowersCache = useRef([]);
|
||||||
async function fetchFollowers(firstLoad) {
|
async function fetchFollowers(firstLoad) {
|
||||||
if (firstLoad || !followersIterator.current) {
|
if (firstLoad || !followersIterator.current) {
|
||||||
followersIterator.current = masto.v1.accounts.listFollowers(id, {
|
followersIterator.current = masto.v1.accounts.$select(id).followers.list({
|
||||||
limit: LIMIT,
|
limit: LIMIT,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -153,9 +223,9 @@ function AccountInfo({
|
||||||
// On first load, fetch familiar followers, merge to top of results' `value`
|
// On first load, fetch familiar followers, merge to top of results' `value`
|
||||||
// Remove dups on every fetch
|
// Remove dups on every fetch
|
||||||
if (firstLoad) {
|
if (firstLoad) {
|
||||||
const familiarFollowers = await masto.v1.accounts.fetchFamiliarFollowers(
|
const familiarFollowers = await masto.v1.accounts
|
||||||
id,
|
.familiarFollowers(id)
|
||||||
);
|
.fetch();
|
||||||
familiarFollowersCache.current = familiarFollowers[0].accounts;
|
familiarFollowersCache.current = familiarFollowers[0].accounts;
|
||||||
newValue = [
|
newValue = [
|
||||||
...familiarFollowersCache.current,
|
...familiarFollowersCache.current,
|
||||||
|
@ -184,7 +254,7 @@ function AccountInfo({
|
||||||
const followingIterator = useRef();
|
const followingIterator = useRef();
|
||||||
async function fetchFollowing(firstLoad) {
|
async function fetchFollowing(firstLoad) {
|
||||||
if (firstLoad || !followingIterator.current) {
|
if (firstLoad || !followingIterator.current) {
|
||||||
followingIterator.current = masto.v1.accounts.listFollowing(id, {
|
followingIterator.current = masto.v1.accounts.$select(id).following.list({
|
||||||
limit: LIMIT,
|
limit: LIMIT,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -195,6 +265,51 @@ function AccountInfo({
|
||||||
const LinkOrDiv = standalone ? 'div' : Link;
|
const LinkOrDiv = standalone ? 'div' : Link;
|
||||||
const accountLink = instance ? `/${instance}/a/${id}` : `/a/${id}`;
|
const accountLink = instance ? `/${instance}/a/${id}` : `/a/${id}`;
|
||||||
|
|
||||||
|
const [familiarFollowers, setFamiliarFollowers] = useState([]);
|
||||||
|
const [postingStats, setPostingStats] = useState();
|
||||||
|
const [postingStatsUIState, setPostingStatsUIState] = useState('default');
|
||||||
|
const hasPostingStats = !!postingStats?.total;
|
||||||
|
|
||||||
|
const renderFamiliarFollowers = async (currentID) => {
|
||||||
|
try {
|
||||||
|
const followers = await memFetchFamiliarFollowers(
|
||||||
|
currentID,
|
||||||
|
currentMasto,
|
||||||
|
);
|
||||||
|
console.log('fetched familiar followers', followers);
|
||||||
|
setFamiliarFollowers(
|
||||||
|
followers[0].accounts.slice(0, FAMILIAR_FOLLOWERS_LIMIT),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderPostingStats = async () => {
|
||||||
|
if (!id) return;
|
||||||
|
setPostingStatsUIState('loading');
|
||||||
|
try {
|
||||||
|
const stats = await memFetchPostingStats(id, masto);
|
||||||
|
setPostingStats(stats);
|
||||||
|
setPostingStatsUIState('default');
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
setPostingStatsUIState('error');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onRelationshipChange = useCallback(
|
||||||
|
({ relationship, currentID }) => {
|
||||||
|
if (!relationship.following) {
|
||||||
|
renderFamiliarFollowers(currentID);
|
||||||
|
if (!standalone) {
|
||||||
|
renderPostingStats();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[standalone, id],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
class={`account-container ${uiState === 'loading' ? 'skeleton' : ''}`}
|
class={`account-container ${uiState === 'loading' ? 'skeleton' : ''}`}
|
||||||
|
@ -229,7 +344,7 @@ function AccountInfo({
|
||||||
<p>████████ ███████</p>
|
<p>████████ ███████</p>
|
||||||
<p>███████████████ ███████████████</p>
|
<p>███████████████ ███████████████</p>
|
||||||
</div>
|
</div>
|
||||||
<p class="stats">
|
<div class="stats">
|
||||||
<div>
|
<div>
|
||||||
<span>██</span> Followers
|
<span>██</span> Followers
|
||||||
</div>
|
</div>
|
||||||
|
@ -240,7 +355,7 @@ function AccountInfo({
|
||||||
<span>██</span> Posts
|
<span>██</span> Posts
|
||||||
</div>
|
</div>
|
||||||
<div>Joined ██</div>
|
<div>Joined ██</div>
|
||||||
</p>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
@ -381,8 +496,20 @@ function AccountInfo({
|
||||||
<Icon icon="group" /> Group
|
<Icon icon="group" /> Group
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
{roles?.map((role) => (
|
||||||
|
<span class="tag">
|
||||||
|
{role.name}
|
||||||
|
{!!accountInstance && (
|
||||||
|
<>
|
||||||
|
{' '}
|
||||||
|
<span class="more-insignificant">{accountInstance}</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
<div
|
<div
|
||||||
class="note"
|
class="note"
|
||||||
|
dir="auto"
|
||||||
onClick={handleContentLinks({
|
onClick={handleContentLinks({
|
||||||
instance,
|
instance,
|
||||||
})}
|
})}
|
||||||
|
@ -399,6 +526,7 @@ function AccountInfo({
|
||||||
verifiedAt ? 'profile-verified' : ''
|
verifiedAt ? 'profile-verified' : ''
|
||||||
}`}
|
}`}
|
||||||
key={name + i}
|
key={name + i}
|
||||||
|
dir="auto"
|
||||||
>
|
>
|
||||||
<b>
|
<b>
|
||||||
<EmojiText text={name} emojis={emojis} />{' '}
|
<EmojiText text={name} emojis={emojis} />{' '}
|
||||||
|
@ -427,19 +555,17 @@ function AccountInfo({
|
||||||
};
|
};
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{!!snapAccountInfoStates.familiarFollowers.length && (
|
{!!familiarFollowers.length && (
|
||||||
<span class="shazam-container-horizontal">
|
<span class="shazam-container-horizontal">
|
||||||
<span class="shazam-container-inner stats-avatars-bunch">
|
<span class="shazam-container-inner stats-avatars-bunch">
|
||||||
{(snapAccountInfoStates.familiarFollowers || []).map(
|
{familiarFollowers.map((follower) => (
|
||||||
(follower) => (
|
<Avatar
|
||||||
<Avatar
|
url={follower.avatarStatic}
|
||||||
url={follower.avatarStatic}
|
size="s"
|
||||||
size="s"
|
alt={`${follower.displayName} @${follower.acct}`}
|
||||||
alt={`${follower.displayName} @${follower.acct}`}
|
squircle={follower?.bot}
|
||||||
squircle={follower?.bot}
|
/>
|
||||||
/>
|
))}
|
||||||
),
|
|
||||||
)}
|
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
@ -494,11 +620,112 @@ function AccountInfo({
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{!!postingStats && (
|
||||||
|
<LinkOrDiv
|
||||||
|
to={accountLink}
|
||||||
|
class="account-metadata-box"
|
||||||
|
onClick={() => {
|
||||||
|
states.showAccount = false;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class="shazam-container">
|
||||||
|
<div class="shazam-container-inner">
|
||||||
|
{hasPostingStats ? (
|
||||||
|
<div
|
||||||
|
class="posting-stats"
|
||||||
|
title={`${Math.round(
|
||||||
|
(postingStats.originals / postingStats.total) * 100,
|
||||||
|
)}% original posts, ${Math.round(
|
||||||
|
(postingStats.replies / postingStats.total) * 100,
|
||||||
|
)}% replies, ${Math.round(
|
||||||
|
(postingStats.boosts / postingStats.total) * 100,
|
||||||
|
)}% boosts`}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
{postingStats.daysSinceLastPost < 365
|
||||||
|
? `Last ${postingStats.total} posts in the past
|
||||||
|
${postingStats.daysSinceLastPost} day${
|
||||||
|
postingStats.daysSinceLastPost > 1 ? 's' : ''
|
||||||
|
}`
|
||||||
|
: `
|
||||||
|
Last ${postingStats.total} posts in the past year(s)
|
||||||
|
`}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="posting-stats-bar"
|
||||||
|
style={{
|
||||||
|
// [originals | replies | boosts]
|
||||||
|
'--originals-percentage': `${
|
||||||
|
(postingStats.originals / postingStats.total) *
|
||||||
|
100
|
||||||
|
}%`,
|
||||||
|
'--replies-percentage': `${
|
||||||
|
((postingStats.originals +
|
||||||
|
postingStats.replies) /
|
||||||
|
postingStats.total) *
|
||||||
|
100
|
||||||
|
}%`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div class="posting-stats-legends">
|
||||||
|
<span class="ib">
|
||||||
|
<span class="posting-stats-legend-item posting-stats-legend-item-originals" />{' '}
|
||||||
|
Original
|
||||||
|
</span>{' '}
|
||||||
|
<span class="ib">
|
||||||
|
<span class="posting-stats-legend-item posting-stats-legend-item-replies" />{' '}
|
||||||
|
Replies
|
||||||
|
</span>{' '}
|
||||||
|
<span class="ib">
|
||||||
|
<span class="posting-stats-legend-item posting-stats-legend-item-boosts" />{' '}
|
||||||
|
Boosts
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div class="posting-stats">Post stats unavailable.</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</LinkOrDiv>
|
||||||
|
)}
|
||||||
|
<div class="account-metadata-box">
|
||||||
|
<div
|
||||||
|
class="shazam-container no-animation"
|
||||||
|
hidden={!!postingStats}
|
||||||
|
>
|
||||||
|
<div class="shazam-container-inner">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="posting-stats-button"
|
||||||
|
disabled={postingStatsUIState === 'loading'}
|
||||||
|
onClick={() => {
|
||||||
|
renderPostingStats();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class={`posting-stats-bar posting-stats-icon ${
|
||||||
|
postingStatsUIState === 'loading' ? 'loading' : ''
|
||||||
|
}`}
|
||||||
|
style={{
|
||||||
|
'--originals-percentage': '33%',
|
||||||
|
'--replies-percentage': '66%',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
View post stats{' '}
|
||||||
|
{/* <Loader
|
||||||
|
abrupt
|
||||||
|
hidden={postingStatsUIState !== 'loading'}
|
||||||
|
/> */}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<RelatedActions
|
<RelatedActions
|
||||||
info={info}
|
info={info}
|
||||||
instance={instance}
|
instance={instance}
|
||||||
authenticated={authenticated}
|
authenticated={authenticated}
|
||||||
standalone={standalone}
|
onRelationshipChange={onRelationshipChange}
|
||||||
/>
|
/>
|
||||||
</main>
|
</main>
|
||||||
</>
|
</>
|
||||||
|
@ -510,7 +737,12 @@ function AccountInfo({
|
||||||
|
|
||||||
const FAMILIAR_FOLLOWERS_LIMIT = 3;
|
const FAMILIAR_FOLLOWERS_LIMIT = 3;
|
||||||
|
|
||||||
function RelatedActions({ info, instance, authenticated, standalone }) {
|
function RelatedActions({
|
||||||
|
info,
|
||||||
|
instance,
|
||||||
|
authenticated,
|
||||||
|
onRelationshipChange = () => {},
|
||||||
|
}) {
|
||||||
if (!info) return null;
|
if (!info) return null;
|
||||||
const {
|
const {
|
||||||
masto: currentMasto,
|
masto: currentMasto,
|
||||||
|
@ -521,7 +753,6 @@ function RelatedActions({ info, instance, authenticated, standalone }) {
|
||||||
|
|
||||||
const [relationshipUIState, setRelationshipUIState] = useState('default');
|
const [relationshipUIState, setRelationshipUIState] = useState('default');
|
||||||
const [relationship, setRelationship] = useState(null);
|
const [relationship, setRelationship] = useState(null);
|
||||||
const [postingStats, setPostingStats] = useState();
|
|
||||||
|
|
||||||
const { id, acct, url, username, locked, lastStatusAt, note, fields, moved } =
|
const { id, acct, url, username, locked, lastStatusAt, note, fields, moved } =
|
||||||
info;
|
info;
|
||||||
|
@ -555,7 +786,7 @@ function RelatedActions({ info, instance, authenticated, standalone }) {
|
||||||
// Grab this account from my logged-in instance
|
// Grab this account from my logged-in instance
|
||||||
const acctHasInstance = info.acct.includes('@');
|
const acctHasInstance = info.acct.includes('@');
|
||||||
try {
|
try {
|
||||||
const results = await currentMasto.v2.search({
|
const results = await currentMasto.v2.search.fetch({
|
||||||
q: acctHasInstance ? info.acct : `${info.username}@${instance}`,
|
q: acctHasInstance ? info.acct : `${info.username}@${instance}`,
|
||||||
type: 'accounts',
|
type: 'accounts',
|
||||||
limit: 1,
|
limit: 1,
|
||||||
|
@ -584,12 +815,12 @@ function RelatedActions({ info, instance, authenticated, standalone }) {
|
||||||
if (moved) return;
|
if (moved) return;
|
||||||
|
|
||||||
setRelationshipUIState('loading');
|
setRelationshipUIState('loading');
|
||||||
accountInfoStates.familiarFollowers = [];
|
|
||||||
setPostingStats(null);
|
|
||||||
|
|
||||||
const fetchRelationships = currentMasto.v1.accounts.fetchRelationships([
|
const fetchRelationships = currentMasto.v1.accounts.relationships.fetch(
|
||||||
currentID,
|
{
|
||||||
]);
|
id: [currentID],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const relationships = await fetchRelationships;
|
const relationships = await fetchRelationships;
|
||||||
|
@ -599,63 +830,7 @@ function RelatedActions({ info, instance, authenticated, standalone }) {
|
||||||
if (relationships.length) {
|
if (relationships.length) {
|
||||||
const relationship = relationships[0];
|
const relationship = relationships[0];
|
||||||
setRelationship(relationship);
|
setRelationship(relationship);
|
||||||
|
onRelationshipChange({ relationship, currentID });
|
||||||
if (!relationship.following) {
|
|
||||||
try {
|
|
||||||
const fetchFamiliarFollowers =
|
|
||||||
currentMasto.v1.accounts.fetchFamiliarFollowers(currentID);
|
|
||||||
const fetchStatuses = currentMasto.v1.accounts
|
|
||||||
.listStatuses(currentID, {
|
|
||||||
limit: 20,
|
|
||||||
})
|
|
||||||
.next();
|
|
||||||
|
|
||||||
const followers = await fetchFamiliarFollowers;
|
|
||||||
console.log('fetched familiar followers', followers);
|
|
||||||
accountInfoStates.familiarFollowers =
|
|
||||||
followers[0].accounts.slice(0, FAMILIAR_FOLLOWERS_LIMIT);
|
|
||||||
|
|
||||||
if (!standalone) {
|
|
||||||
const { value: statuses } = await fetchStatuses;
|
|
||||||
console.log('fetched statuses', statuses);
|
|
||||||
const stats = {
|
|
||||||
total: statuses.length,
|
|
||||||
originals: 0,
|
|
||||||
replies: 0,
|
|
||||||
boosts: 0,
|
|
||||||
};
|
|
||||||
// Categories statuses by type
|
|
||||||
// - Original posts (not replies to others)
|
|
||||||
// - Threads (self-replies + 1st original post)
|
|
||||||
// - Boosts (reblogs)
|
|
||||||
// - Replies (not-self replies)
|
|
||||||
statuses.forEach((status) => {
|
|
||||||
if (status.reblog) {
|
|
||||||
stats.boosts++;
|
|
||||||
} else if (
|
|
||||||
status.inReplyToAccountId !== currentID &&
|
|
||||||
!!status.inReplyToId
|
|
||||||
) {
|
|
||||||
stats.replies++;
|
|
||||||
} else {
|
|
||||||
stats.originals++;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Count days since last post
|
|
||||||
stats.daysSinceLastPost = Math.ceil(
|
|
||||||
(Date.now() -
|
|
||||||
new Date(statuses[statuses.length - 1].createdAt)) /
|
|
||||||
86400000,
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log('posting stats', stats);
|
|
||||||
setPostingStats(stats);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
|
@ -677,75 +852,9 @@ function RelatedActions({ info, instance, authenticated, standalone }) {
|
||||||
const [showTranslatedBio, setShowTranslatedBio] = useState(false);
|
const [showTranslatedBio, setShowTranslatedBio] = useState(false);
|
||||||
const [showAddRemoveLists, setShowAddRemoveLists] = useState(false);
|
const [showAddRemoveLists, setShowAddRemoveLists] = useState(false);
|
||||||
|
|
||||||
const hasPostingStats = postingStats?.total >= 3;
|
|
||||||
const accountLink = instance ? `/${instance}/a/${id}` : `/a/${id}`;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{hasPostingStats && (
|
<div class="actions">
|
||||||
<Link
|
|
||||||
to={accountLink}
|
|
||||||
class="account-metadata-box"
|
|
||||||
onClick={() => {
|
|
||||||
states.showAccount = false;
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div class="shazam-container">
|
|
||||||
<div class="shazam-container-inner">
|
|
||||||
<div
|
|
||||||
class="posting-stats"
|
|
||||||
title={`${Math.round(
|
|
||||||
(postingStats.originals / postingStats.total) * 100,
|
|
||||||
)}% original posts, ${Math.round(
|
|
||||||
(postingStats.replies / postingStats.total) * 100,
|
|
||||||
)}% replies, ${Math.round(
|
|
||||||
(postingStats.boosts / postingStats.total) * 100,
|
|
||||||
)}% boosts`}
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
{postingStats.daysSinceLastPost < 365
|
|
||||||
? `Last ${postingStats.total} posts in the past
|
|
||||||
${postingStats.daysSinceLastPost} day${
|
|
||||||
postingStats.daysSinceLastPost > 1 ? 's' : ''
|
|
||||||
}`
|
|
||||||
: `
|
|
||||||
Last ${postingStats.total} posts in the past year(s)
|
|
||||||
`}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="posting-stats-bar"
|
|
||||||
style={{
|
|
||||||
// [originals | replies | boosts]
|
|
||||||
'--originals-percentage': `${
|
|
||||||
(postingStats.originals / postingStats.total) * 100
|
|
||||||
}%`,
|
|
||||||
'--replies-percentage': `${
|
|
||||||
((postingStats.originals + postingStats.replies) /
|
|
||||||
postingStats.total) *
|
|
||||||
100
|
|
||||||
}%`,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div class="posting-stats-legends">
|
|
||||||
<span class="ib">
|
|
||||||
<span class="posting-stats-legend-item posting-stats-legend-item-originals" />{' '}
|
|
||||||
Original
|
|
||||||
</span>{' '}
|
|
||||||
<span class="ib">
|
|
||||||
<span class="posting-stats-legend-item posting-stats-legend-item-replies" />{' '}
|
|
||||||
Replies
|
|
||||||
</span>{' '}
|
|
||||||
<span class="ib">
|
|
||||||
<span class="posting-stats-legend-item posting-stats-legend-item-boosts" />{' '}
|
|
||||||
Boosts
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
<p class="actions">
|
|
||||||
<span>
|
<span>
|
||||||
{followedBy ? (
|
{followedBy ? (
|
||||||
<span class="tag">Following you</span>
|
<span class="tag">Following you</span>
|
||||||
|
@ -880,14 +989,15 @@ function RelatedActions({ info, instance, authenticated, standalone }) {
|
||||||
setRelationshipUIState('loading');
|
setRelationshipUIState('loading');
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
const newRelationship =
|
const newRelationship = await currentMasto.v1.accounts
|
||||||
await currentMasto.v1.accounts.unmute(
|
.$select(currentInfo?.id || id)
|
||||||
currentInfo?.id || id,
|
.unmute();
|
||||||
);
|
|
||||||
console.log('unmuting', newRelationship);
|
console.log('unmuting', newRelationship);
|
||||||
setRelationship(newRelationship);
|
setRelationship(newRelationship);
|
||||||
setRelationshipUIState('default');
|
setRelationshipUIState('default');
|
||||||
showToast(`Unmuted @${username}`);
|
showToast(`Unmuted @${username}`);
|
||||||
|
states.reloadGenericAccounts.id = 'mute';
|
||||||
|
states.reloadGenericAccounts.counter++;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
setRelationshipUIState('error');
|
setRelationshipUIState('error');
|
||||||
|
@ -927,18 +1037,19 @@ function RelatedActions({ info, instance, authenticated, standalone }) {
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
const newRelationship =
|
const newRelationship =
|
||||||
await currentMasto.v1.accounts.mute(
|
await currentMasto.v1.accounts
|
||||||
currentInfo?.id || id,
|
.$select(currentInfo?.id || id)
|
||||||
{
|
.mute({
|
||||||
duration,
|
duration,
|
||||||
},
|
});
|
||||||
);
|
|
||||||
console.log('muting', newRelationship);
|
console.log('muting', newRelationship);
|
||||||
setRelationship(newRelationship);
|
setRelationship(newRelationship);
|
||||||
setRelationshipUIState('default');
|
setRelationshipUIState('default');
|
||||||
showToast(
|
showToast(
|
||||||
`Muted @${username} for ${MUTE_DURATIONS_LABELS[duration]}`,
|
`Muted @${username} for ${MUTE_DURATIONS_LABELS[duration]}`,
|
||||||
);
|
);
|
||||||
|
states.reloadGenericAccounts.id = 'mute';
|
||||||
|
states.reloadGenericAccounts.counter++;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
setRelationshipUIState('error');
|
setRelationshipUIState('error');
|
||||||
|
@ -971,24 +1082,24 @@ function RelatedActions({ info, instance, authenticated, standalone }) {
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
if (blocking) {
|
if (blocking) {
|
||||||
const newRelationship =
|
const newRelationship = await currentMasto.v1.accounts
|
||||||
await currentMasto.v1.accounts.unblock(
|
.$select(currentInfo?.id || id)
|
||||||
currentInfo?.id || id,
|
.unblock();
|
||||||
);
|
|
||||||
console.log('unblocking', newRelationship);
|
console.log('unblocking', newRelationship);
|
||||||
setRelationship(newRelationship);
|
setRelationship(newRelationship);
|
||||||
setRelationshipUIState('default');
|
setRelationshipUIState('default');
|
||||||
showToast(`Unblocked @${username}`);
|
showToast(`Unblocked @${username}`);
|
||||||
} else {
|
} else {
|
||||||
const newRelationship =
|
const newRelationship = await currentMasto.v1.accounts
|
||||||
await currentMasto.v1.accounts.block(
|
.$select(currentInfo?.id || id)
|
||||||
currentInfo?.id || id,
|
.block();
|
||||||
);
|
|
||||||
console.log('blocking', newRelationship);
|
console.log('blocking', newRelationship);
|
||||||
setRelationship(newRelationship);
|
setRelationship(newRelationship);
|
||||||
setRelationshipUIState('default');
|
setRelationshipUIState('default');
|
||||||
showToast(`Blocked @${username}`);
|
showToast(`Blocked @${username}`);
|
||||||
}
|
}
|
||||||
|
states.reloadGenericAccounts.id = 'block';
|
||||||
|
states.reloadGenericAccounts.counter++;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
setRelationshipUIState('error');
|
setRelationshipUIState('error');
|
||||||
|
@ -1050,14 +1161,14 @@ function RelatedActions({ info, instance, authenticated, standalone }) {
|
||||||
// );
|
// );
|
||||||
|
|
||||||
// if (yes) {
|
// if (yes) {
|
||||||
newRelationship = await currentMasto.v1.accounts.unfollow(
|
newRelationship = await currentMasto.v1.accounts
|
||||||
accountID.current,
|
.$select(accountID.current)
|
||||||
);
|
.unfollow();
|
||||||
// }
|
// }
|
||||||
} else {
|
} else {
|
||||||
newRelationship = await currentMasto.v1.accounts.follow(
|
newRelationship = await currentMasto.v1.accounts
|
||||||
accountID.current,
|
.$select(accountID.current)
|
||||||
);
|
.follow();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newRelationship) setRelationship(newRelationship);
|
if (newRelationship) setRelationship(newRelationship);
|
||||||
|
@ -1096,7 +1207,7 @@ function RelatedActions({ info, instance, authenticated, standalone }) {
|
||||||
</MenuConfirm>
|
</MenuConfirm>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</div>
|
||||||
{!!showTranslatedBio && (
|
{!!showTranslatedBio && (
|
||||||
<Modal
|
<Modal
|
||||||
class="light"
|
class="light"
|
||||||
|
@ -1202,9 +1313,9 @@ function AddRemoveListsSheet({ accountID, onClose }) {
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
const lists = await masto.v1.lists.list();
|
const lists = await masto.v1.lists.list();
|
||||||
const listsContainingAccount = await masto.v1.accounts.listLists(
|
const listsContainingAccount = await masto.v1.accounts
|
||||||
accountID,
|
.$select(accountID)
|
||||||
);
|
.lists.list();
|
||||||
console.log({ lists, listsContainingAccount });
|
console.log({ lists, listsContainingAccount });
|
||||||
setLists(lists);
|
setLists(lists);
|
||||||
setListsContainingAccount(listsContainingAccount);
|
setListsContainingAccount(listsContainingAccount);
|
||||||
|
@ -1246,13 +1357,17 @@ function AddRemoveListsSheet({ accountID, onClose }) {
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
if (inList) {
|
if (inList) {
|
||||||
await masto.v1.lists.removeAccount(list.id, {
|
await masto.v1.lists
|
||||||
accountIds: [accountID],
|
.$select(list.id)
|
||||||
});
|
.accounts.remove({
|
||||||
|
accountIds: [accountID],
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
await masto.v1.lists.addAccount(list.id, {
|
await masto.v1.lists
|
||||||
accountIds: [accountID],
|
.$select(list.id)
|
||||||
});
|
.accounts.create({
|
||||||
|
accountIds: [accountID],
|
||||||
|
});
|
||||||
}
|
}
|
||||||
// setUIState('default');
|
// setUIState('default');
|
||||||
reload();
|
reload();
|
||||||
|
|
|
@ -46,7 +46,7 @@ function AccountSheet({ account, instance: propInstance, onClose }) {
|
||||||
});
|
});
|
||||||
return info;
|
return info;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const result = await masto.v2.search({
|
const result = await masto.v2.search.fetch({
|
||||||
q: account,
|
q: account,
|
||||||
type: 'accounts',
|
type: 'accounts',
|
||||||
limit: 1,
|
limit: 1,
|
||||||
|
@ -57,7 +57,7 @@ function AccountSheet({ account, instance: propInstance, onClose }) {
|
||||||
} else if (/https?:\/\/[^/]+\/@/.test(account)) {
|
} else if (/https?:\/\/[^/]+\/@/.test(account)) {
|
||||||
const accountURL = new URL(account);
|
const accountURL = new URL(account);
|
||||||
const acct = accountURL.pathname.replace(/^\//, '');
|
const acct = accountURL.pathname.replace(/^\//, '');
|
||||||
const result = await masto.v2.search({
|
const result = await masto.v2.search.fetch({
|
||||||
q: acct,
|
q: acct,
|
||||||
type: 'accounts',
|
type: 'accounts',
|
||||||
limit: 1,
|
limit: 1,
|
||||||
|
|
|
@ -11,10 +11,10 @@ export default memo(function BackgroundService({ isLoggedIn }) {
|
||||||
// - WebSocket to receive notifications when page is visible
|
// - WebSocket to receive notifications when page is visible
|
||||||
const [visible, setVisible] = useState(true);
|
const [visible, setVisible] = useState(true);
|
||||||
usePageVisibility(setVisible);
|
usePageVisibility(setVisible);
|
||||||
const notificationStream = useRef();
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
let sub;
|
||||||
if (isLoggedIn && visible) {
|
if (isLoggedIn && visible) {
|
||||||
const { masto, instance } = api();
|
const { masto, streaming, instance } = api();
|
||||||
(async () => {
|
(async () => {
|
||||||
// 1. Get the latest notification
|
// 1. Get the latest notification
|
||||||
if (states.notificationsLast) {
|
if (states.notificationsLast) {
|
||||||
|
@ -42,34 +42,26 @@ export default memo(function BackgroundService({ isLoggedIn }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Start streaming
|
// 2. Start streaming
|
||||||
notificationStream.current = await masto.ws.stream(
|
if (streaming) {
|
||||||
'/api/v1/streaming',
|
sub = streaming.user.notification.subscribe();
|
||||||
{
|
console.log('🎏 Streaming notification', sub);
|
||||||
stream: 'user:notification',
|
for await (const entry of sub) {
|
||||||
},
|
if (!sub) break;
|
||||||
);
|
console.log('🔔🔔 Notification entry', entry);
|
||||||
console.log('🎏 Streaming notification', notificationStream.current);
|
if (entry.event === 'notification') {
|
||||||
|
console.log('🔔🔔 Notification', entry);
|
||||||
notificationStream.current.on('notification', (notification) => {
|
saveStatus(entry.payload, instance, {
|
||||||
console.log('🔔🔔 Notification', notification);
|
skipThreading: true,
|
||||||
if (notification.status) {
|
});
|
||||||
saveStatus(notification.status, instance, {
|
}
|
||||||
skipThreading: true,
|
states.notificationsShowNew = true;
|
||||||
});
|
|
||||||
}
|
}
|
||||||
states.notificationsShowNew = true;
|
}
|
||||||
});
|
|
||||||
|
|
||||||
notificationStream.current.ws.onclose = () => {
|
|
||||||
console.log('🔔🔔 Notification stream closed');
|
|
||||||
};
|
|
||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
return () => {
|
return () => {
|
||||||
if (notificationStream.current) {
|
sub?.unsubscribe?.();
|
||||||
notificationStream.current.ws.close();
|
sub = null;
|
||||||
notificationStream.current = null;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}, [visible, isLoggedIn]);
|
}, [visible, isLoggedIn]);
|
||||||
|
|
||||||
|
|
|
@ -487,7 +487,28 @@
|
||||||
padding-inline: 24px;
|
padding-inline: 24px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#media-sheet {
|
||||||
|
.media-form {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
min-height: 50vh;
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
flex-grow: 1;
|
||||||
|
resize: none;
|
||||||
|
width: 100%;
|
||||||
|
/* height: 10em; */
|
||||||
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
#media-sheet main {
|
#media-sheet main {
|
||||||
padding-top: 8px;
|
padding-top: 8px;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -495,10 +516,6 @@
|
||||||
flex: 1;
|
flex: 1;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
#media-sheet textarea {
|
|
||||||
width: 100%;
|
|
||||||
height: 10em;
|
|
||||||
}
|
|
||||||
#media-sheet .media-preview {
|
#media-sheet .media-preview {
|
||||||
border: 2px solid var(--outline-color);
|
border: 2px solid var(--outline-color);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
@ -515,6 +532,7 @@
|
||||||
linear-gradient(-45deg, transparent 75%, var(--img-bg-color) 75%);
|
linear-gradient(-45deg, transparent 75%, var(--img-bg-color) 75%);
|
||||||
background-size: 20px 20px;
|
background-size: 20px 20px;
|
||||||
background-position: 0 0, 0 10px, 10px -10px, -10px 0px;
|
background-position: 0 0, 0 10px, 10px -10px, -10px 0px;
|
||||||
|
flex: 0.8;
|
||||||
}
|
}
|
||||||
#media-sheet .media-preview > * {
|
#media-sheet .media-preview > * {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -534,11 +552,11 @@
|
||||||
#media-sheet .media-preview > * {
|
#media-sheet .media-preview > * {
|
||||||
max-height: none;
|
max-height: none;
|
||||||
}
|
}
|
||||||
#media-sheet textarea {
|
/* #media-sheet textarea {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
height: auto;
|
height: auto;
|
||||||
}
|
} */
|
||||||
}
|
}
|
||||||
|
|
||||||
#custom-emojis-sheet {
|
#custom-emojis-sheet {
|
||||||
|
|
|
@ -185,7 +185,7 @@ function Compose({
|
||||||
: visibility,
|
: visibility,
|
||||||
);
|
);
|
||||||
setLanguage(language || prefs.postingDefaultLanguage || DEFAULT_LANG);
|
setLanguage(language || prefs.postingDefaultLanguage || DEFAULT_LANG);
|
||||||
setSensitive(sensitive);
|
setSensitive(sensitive && !!spoilerText);
|
||||||
} else if (editStatus) {
|
} else if (editStatus) {
|
||||||
const { visibility, language, sensitive, poll, mediaAttachments } =
|
const { visibility, language, sensitive, poll, mediaAttachments } =
|
||||||
editStatus;
|
editStatus;
|
||||||
|
@ -197,9 +197,9 @@ function Compose({
|
||||||
setUIState('loading');
|
setUIState('loading');
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
const statusSource = await masto.v1.statuses.fetchSource(
|
const statusSource = await masto.v1.statuses
|
||||||
editStatus.id,
|
.$select(editStatus.id)
|
||||||
);
|
.source.fetch();
|
||||||
console.log({ statusSource });
|
console.log({ statusSource });
|
||||||
const { text, spoilerText } = statusSource;
|
const { text, spoilerText } = statusSource;
|
||||||
textareaRef.current.value = text;
|
textareaRef.current.value = text;
|
||||||
|
@ -749,14 +749,12 @@ function Compose({
|
||||||
file,
|
file,
|
||||||
description,
|
description,
|
||||||
});
|
});
|
||||||
return masto.v2.mediaAttachments
|
return masto.v2.media.create(params).then((res) => {
|
||||||
.create(params)
|
if (res.id) {
|
||||||
.then((res) => {
|
attachment.id = res.id;
|
||||||
if (res.id) {
|
}
|
||||||
attachment.id = res.id;
|
return res;
|
||||||
}
|
});
|
||||||
return res;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const results = await Promise.allSettled(mediaPromises);
|
const results = await Promise.allSettled(mediaPromises);
|
||||||
|
@ -784,6 +782,8 @@ function Compose({
|
||||||
/* NOTE:
|
/* NOTE:
|
||||||
Using snakecase here because masto.js's `isObject` returns false for `params`, ONLY happens when opening in pop-out window. This is maybe due to `window.masto` variable being passed from the parent window. The check that failed is `x.constructor === Object`, so maybe the `Object` in new window is different than parent window's?
|
Using snakecase here because masto.js's `isObject` returns false for `params`, ONLY happens when opening in pop-out window. This is maybe due to `window.masto` variable being passed from the parent window. The check that failed is `x.constructor === Object`, so maybe the `Object` in new window is different than parent window's?
|
||||||
Code: https://github.com/neet/masto.js/blob/dd0d649067b6a2b6e60fbb0a96597c373a255b00/src/serializers/is-object.ts#L2
|
Code: https://github.com/neet/masto.js/blob/dd0d649067b6a2b6e60fbb0a96597c373a255b00/src/serializers/is-object.ts#L2
|
||||||
|
|
||||||
|
// TODO: Note above is no longer true in Masto.js v6. Revisit this.
|
||||||
*/
|
*/
|
||||||
let params = {
|
let params = {
|
||||||
status,
|
status,
|
||||||
|
@ -818,10 +818,9 @@ function Compose({
|
||||||
|
|
||||||
let newStatus;
|
let newStatus;
|
||||||
if (editStatus) {
|
if (editStatus) {
|
||||||
newStatus = await masto.v1.statuses.update(
|
newStatus = await masto.v1.statuses
|
||||||
editStatus.id,
|
.$select(editStatus.id)
|
||||||
params,
|
.update(params);
|
||||||
);
|
|
||||||
saveStatus(newStatus, instance, {
|
saveStatus(newStatus, instance, {
|
||||||
skipThreading: true,
|
skipThreading: true,
|
||||||
});
|
});
|
||||||
|
@ -839,6 +838,8 @@ function Compose({
|
||||||
|
|
||||||
// Close
|
// Close
|
||||||
onClose({
|
onClose({
|
||||||
|
// type: post, reply, edit
|
||||||
|
type: editStatus ? 'edit' : replyToStatus ? 'reply' : 'post',
|
||||||
newStatus,
|
newStatus,
|
||||||
instance,
|
instance,
|
||||||
});
|
});
|
||||||
|
@ -933,13 +934,13 @@ function Compose({
|
||||||
performSearch={(params) => {
|
performSearch={(params) => {
|
||||||
const { type, q, limit } = params;
|
const { type, q, limit } = params;
|
||||||
if (type === 'accounts') {
|
if (type === 'accounts') {
|
||||||
return masto.v1.accounts.search({
|
return masto.v1.accounts.search.list({
|
||||||
q,
|
q,
|
||||||
limit,
|
limit,
|
||||||
resolve: false,
|
resolve: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return masto.v2.search(params);
|
return masto.v2.search.fetch(params);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{mediaAttachments?.length > 0 && (
|
{mediaAttachments?.length > 0 && (
|
||||||
|
@ -1475,7 +1476,11 @@ function MediaAttachment({
|
||||||
onRemove = () => {},
|
onRemove = () => {},
|
||||||
}) {
|
}) {
|
||||||
const supportsEdit = supports('@mastodon/edit-media-attributes');
|
const supportsEdit = supports('@mastodon/edit-media-attributes');
|
||||||
const { url, type, id } = attachment;
|
const { type, id, file } = attachment;
|
||||||
|
const url = useMemo(
|
||||||
|
() => (file ? URL.createObjectURL(file) : attachment.url),
|
||||||
|
[file, attachment.url],
|
||||||
|
);
|
||||||
console.log({ attachment });
|
console.log({ attachment });
|
||||||
const [description, setDescription] = useState(attachment.description);
|
const [description, setDescription] = useState(attachment.description);
|
||||||
const suffixType = type.split('/')[0];
|
const suffixType = type.split('/')[0];
|
||||||
|
@ -1542,6 +1547,7 @@ function MediaAttachment({
|
||||||
<div class="media-attachment">
|
<div class="media-attachment">
|
||||||
<div
|
<div
|
||||||
class="media-preview"
|
class="media-preview"
|
||||||
|
tabIndex="0"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setShowModal(true);
|
setShowModal(true);
|
||||||
}}
|
}}
|
||||||
|
@ -1568,6 +1574,7 @@ function MediaAttachment({
|
||||||
</div>
|
</div>
|
||||||
{showModal && (
|
{showModal && (
|
||||||
<Modal
|
<Modal
|
||||||
|
class="light"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
if (e.target === e.currentTarget) {
|
if (e.target === e.currentTarget) {
|
||||||
setShowModal(false);
|
setShowModal(false);
|
||||||
|
@ -1605,7 +1612,20 @@ function MediaAttachment({
|
||||||
<audio src={url} controls />
|
<audio src={url} controls />
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
{descTextarea}
|
<div class="media-form">
|
||||||
|
{descTextarea}
|
||||||
|
<footer>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="light block"
|
||||||
|
onClick={() => {
|
||||||
|
setShowModal(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Done
|
||||||
|
</button>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
|
@ -128,9 +128,9 @@ function Drafts({ onClose }) {
|
||||||
if (replyTo) {
|
if (replyTo) {
|
||||||
setUIState('loading');
|
setUIState('loading');
|
||||||
try {
|
try {
|
||||||
replyToStatus = await masto.v1.statuses.fetch(
|
replyToStatus = await masto.v1.statuses
|
||||||
replyTo.id,
|
.$select(replyTo.id)
|
||||||
);
|
.fetch();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
alert('Error fetching reply-to status!');
|
alert('Error fetching reply-to status!');
|
||||||
|
|
|
@ -17,13 +17,15 @@ function FollowRequestButtons({ accountID, onChange }) {
|
||||||
<p class="follow-request-buttons">
|
<p class="follow-request-buttons">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
disabled={uiState === 'loading'}
|
disabled={uiState === 'loading' || hasRelationship}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setUIState('loading');
|
setUIState('loading');
|
||||||
setRequestState('accept');
|
setRequestState('accept');
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
const rel = await masto.v1.followRequests.authorize(accountID);
|
const rel = await masto.v1.followRequests
|
||||||
|
.$select(accountID)
|
||||||
|
.authorize();
|
||||||
if (!rel?.followedBy) {
|
if (!rel?.followedBy) {
|
||||||
throw new Error('Follow request not accepted');
|
throw new Error('Follow request not accepted');
|
||||||
}
|
}
|
||||||
|
@ -40,14 +42,16 @@ function FollowRequestButtons({ accountID, onChange }) {
|
||||||
</button>{' '}
|
</button>{' '}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
disabled={uiState === 'loading'}
|
disabled={uiState === 'loading' || hasRelationship}
|
||||||
class="light danger"
|
class="light danger"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setUIState('loading');
|
setUIState('loading');
|
||||||
setRequestState('reject');
|
setRequestState('reject');
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
const rel = await masto.v1.followRequests.reject(accountID);
|
const rel = await masto.v1.followRequests
|
||||||
|
.$select(accountID)
|
||||||
|
.reject();
|
||||||
if (rel?.followedBy) {
|
if (rel?.followedBy) {
|
||||||
throw new Error('Follow request not rejected');
|
throw new Error('Follow request not rejected');
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,7 @@ export default function GenericAccounts({ onClose = () => {} }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
id,
|
||||||
heading,
|
heading,
|
||||||
fetchAccounts,
|
fetchAccounts,
|
||||||
accounts: staticAccounts,
|
accounts: staticAccounts,
|
||||||
|
@ -60,6 +61,14 @@ export default function GenericAccounts({ onClose = () => {} }) {
|
||||||
}
|
}
|
||||||
}, [staticAccounts, fetchAccounts]);
|
}, [staticAccounts, fetchAccounts]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// reloadGenericAccounts contains value like {id: 'mute', counter: 1}
|
||||||
|
// We only need to reload if the id matches
|
||||||
|
if (snapStates.reloadGenericAccounts?.id === id) {
|
||||||
|
loadAccounts(true);
|
||||||
|
}
|
||||||
|
}, [snapStates.reloadGenericAccounts.counter]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div id="generic-accounts-container" class="sheet" tabindex="-1">
|
<div id="generic-accounts-container" class="sheet" tabindex="-1">
|
||||||
<button type="button" class="sheet-close" onClick={onClose}>
|
<button type="button" class="sheet-close" onClick={onClose}>
|
||||||
|
|
|
@ -100,6 +100,7 @@ export const ICONS = {
|
||||||
'account-edit': () => import('@iconify-icons/mingcute/user-edit-line'),
|
'account-edit': () => import('@iconify-icons/mingcute/user-edit-line'),
|
||||||
'account-warning': () => import('@iconify-icons/mingcute/user-warning-line'),
|
'account-warning': () => import('@iconify-icons/mingcute/user-warning-line'),
|
||||||
keyboard: () => import('@iconify-icons/mingcute/keyboard-line'),
|
keyboard: () => import('@iconify-icons/mingcute/keyboard-line'),
|
||||||
|
cloud: () => import('@iconify-icons/mingcute/cloud-line'),
|
||||||
};
|
};
|
||||||
|
|
||||||
function Icon({
|
function Icon({
|
||||||
|
@ -126,7 +127,7 @@ function Icon({
|
||||||
}, [iconBlock]);
|
}, [iconBlock]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<span
|
||||||
class={`icon ${className}`}
|
class={`icon ${className}`}
|
||||||
title={title || alt}
|
title={title || alt}
|
||||||
style={{
|
style={{
|
||||||
|
@ -151,7 +152,7 @@ function Icon({
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -56,7 +56,7 @@ function ListAddEdit({ list, onClose }) {
|
||||||
let listResult;
|
let listResult;
|
||||||
|
|
||||||
if (editMode) {
|
if (editMode) {
|
||||||
listResult = await masto.v1.lists.update(list.id, {
|
listResult = await masto.v1.lists.$select(list.id).update({
|
||||||
title,
|
title,
|
||||||
replies_policy: repliesPolicy,
|
replies_policy: repliesPolicy,
|
||||||
exclusive,
|
exclusive,
|
||||||
|
@ -141,7 +141,7 @@ function ListAddEdit({ list, onClose }) {
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
await masto.v1.lists.remove(list.id);
|
await masto.v1.lists.$select(list.id).remove();
|
||||||
setUIState('default');
|
setUIState('default');
|
||||||
onClose?.({
|
onClose?.({
|
||||||
state: 'deleted',
|
state: 'deleted',
|
||||||
|
|
74
src/components/media-alt-modal.jsx
Normal file
74
src/components/media-alt-modal.jsx
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
import { Menu, MenuItem } from '@szhsin/react-menu';
|
||||||
|
import { useState } from 'preact/hooks';
|
||||||
|
import { useSnapshot } from 'valtio';
|
||||||
|
|
||||||
|
import getTranslateTargetLanguage from '../utils/get-translate-target-language';
|
||||||
|
import localeMatch from '../utils/locale-match';
|
||||||
|
import states from '../utils/states';
|
||||||
|
|
||||||
|
import Icon from './icon';
|
||||||
|
import TranslationBlock from './translation-block';
|
||||||
|
|
||||||
|
export default function MediaAltModal({ alt, lang, onClose }) {
|
||||||
|
const snapStates = useSnapshot(states);
|
||||||
|
const [forceTranslate, setForceTranslate] = useState(false);
|
||||||
|
const targetLanguage = getTranslateTargetLanguage(true);
|
||||||
|
const contentTranslationHideLanguages =
|
||||||
|
snapStates.settings.contentTranslationHideLanguages || [];
|
||||||
|
const differentLanguage =
|
||||||
|
!!lang &&
|
||||||
|
lang !== targetLanguage &&
|
||||||
|
!localeMatch([lang], [targetLanguage]) &&
|
||||||
|
!contentTranslationHideLanguages.find(
|
||||||
|
(l) => lang === l || localeMatch([lang], [l]),
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="sheet">
|
||||||
|
{!!onClose && (
|
||||||
|
<button type="button" class="sheet-close outer" onClick={onClose}>
|
||||||
|
<Icon icon="x" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<header class="header-grid">
|
||||||
|
<h2>Media description</h2>
|
||||||
|
<div class="header-side">
|
||||||
|
<Menu
|
||||||
|
align="end"
|
||||||
|
menuButton={
|
||||||
|
<button type="button" class="plain4">
|
||||||
|
<Icon icon="more" alt="More" size="xl" />
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<MenuItem
|
||||||
|
disabled={forceTranslate}
|
||||||
|
onClick={() => {
|
||||||
|
setForceTranslate(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon icon="translate" />
|
||||||
|
<span>Translate</span>
|
||||||
|
</MenuItem>
|
||||||
|
</Menu>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<main lang={lang} dir="auto">
|
||||||
|
<p
|
||||||
|
style={{
|
||||||
|
whiteSpace: 'pre-wrap',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{alt}
|
||||||
|
</p>
|
||||||
|
{(differentLanguage || forceTranslate) && (
|
||||||
|
<TranslationBlock
|
||||||
|
forceTranslate={forceTranslate}
|
||||||
|
sourceLanguage={lang}
|
||||||
|
text={alt}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
import { Menu, MenuItem } from '@szhsin/react-menu';
|
import { Menu } from '@szhsin/react-menu';
|
||||||
import { getBlurHashAverageColor } from 'fast-blurhash';
|
import { getBlurHashAverageColor } from 'fast-blurhash';
|
||||||
import { useEffect, useLayoutEffect, useRef, useState } from 'preact/hooks';
|
import { useEffect, useLayoutEffect, useRef, useState } from 'preact/hooks';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
|
@ -6,14 +6,15 @@ import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
import Icon from './icon';
|
import Icon from './icon';
|
||||||
import Link from './link';
|
import Link from './link';
|
||||||
import Media from './media';
|
import Media from './media';
|
||||||
|
import MediaAltModal from './media-alt-modal';
|
||||||
import MenuLink from './menu-link';
|
import MenuLink from './menu-link';
|
||||||
import Modal from './modal';
|
import Modal from './modal';
|
||||||
import TranslationBlock from './translation-block';
|
|
||||||
|
|
||||||
function MediaModal({
|
function MediaModal({
|
||||||
mediaAttachments,
|
mediaAttachments,
|
||||||
statusID,
|
statusID,
|
||||||
instance,
|
instance,
|
||||||
|
lang,
|
||||||
index = 0,
|
index = 0,
|
||||||
onClose = () => {},
|
onClose = () => {},
|
||||||
}) {
|
}) {
|
||||||
|
@ -138,14 +139,19 @@ function MediaModal({
|
||||||
class="media-alt"
|
class="media-alt"
|
||||||
hidden={!showControls}
|
hidden={!showControls}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setShowMediaAlt(media.description);
|
setShowMediaAlt({
|
||||||
|
alt: media.description,
|
||||||
|
lang,
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon icon="info" />
|
<span class="alt-badge">ALT</span>
|
||||||
<span class="media-alt-desc">{media.description}</span>
|
<span class="media-alt-desc" lang={lang} dir="auto">
|
||||||
|
{media.description}
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<Media media={media} showOriginal />
|
<Media media={media} showOriginal lang={lang} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
@ -279,7 +285,8 @@ function MediaModal({
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<MediaAltModal
|
<MediaAltModal
|
||||||
alt={showMediaAlt}
|
alt={showMediaAlt.alt || showMediaAlt}
|
||||||
|
lang={showMediaAlt?.lang}
|
||||||
onClose={() => setShowMediaAlt(false)}
|
onClose={() => setShowMediaAlt(false)}
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
@ -288,52 +295,4 @@ function MediaModal({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function MediaAltModal({ alt, onClose }) {
|
|
||||||
const [forceTranslate, setForceTranslate] = useState(false);
|
|
||||||
return (
|
|
||||||
<div class="sheet">
|
|
||||||
{!!onClose && (
|
|
||||||
<button type="button" class="sheet-close outer" onClick={onClose}>
|
|
||||||
<Icon icon="x" />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
<header class="header-grid">
|
|
||||||
<h2>Media description</h2>
|
|
||||||
<div class="header-side">
|
|
||||||
<Menu
|
|
||||||
align="end"
|
|
||||||
menuButton={
|
|
||||||
<button type="button" class="plain4">
|
|
||||||
<Icon icon="more" alt="More" size="xl" />
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<MenuItem
|
|
||||||
disabled={forceTranslate}
|
|
||||||
onClick={() => {
|
|
||||||
setForceTranslate(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Icon icon="translate" />
|
|
||||||
<span>Translate</span>
|
|
||||||
</MenuItem>
|
|
||||||
</Menu>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
<main>
|
|
||||||
<p
|
|
||||||
style={{
|
|
||||||
whiteSpace: 'pre-wrap',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{alt}
|
|
||||||
</p>
|
|
||||||
{forceTranslate && (
|
|
||||||
<TranslationBlock forceTranslate={forceTranslate} text={alt} />
|
|
||||||
)}
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default MediaModal;
|
export default MediaModal;
|
||||||
|
|
|
@ -9,6 +9,9 @@ import {
|
||||||
} from 'preact/hooks';
|
} from 'preact/hooks';
|
||||||
import QuickPinchZoom, { make3dTransformValue } from 'react-quick-pinch-zoom';
|
import QuickPinchZoom, { make3dTransformValue } from 'react-quick-pinch-zoom';
|
||||||
|
|
||||||
|
import mem from '../utils/mem';
|
||||||
|
import states from '../utils/states';
|
||||||
|
|
||||||
import Icon from './icon';
|
import Icon from './icon';
|
||||||
import Link from './link';
|
import Link from './link';
|
||||||
import { formatDuration } from './status';
|
import { formatDuration } from './status';
|
||||||
|
@ -25,7 +28,49 @@ video = Video clip
|
||||||
audio = Audio track
|
audio = Audio track
|
||||||
*/
|
*/
|
||||||
|
|
||||||
function Media({ media, to, showOriginal, autoAnimate, onClick = () => {} }) {
|
const dataAltLabel = 'ALT';
|
||||||
|
const AltBadge = (props) => {
|
||||||
|
const { alt, lang, index, ...rest } = props;
|
||||||
|
if (!alt || !alt.trim()) return null;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="alt-badge clickable"
|
||||||
|
{...rest}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
states.showMediaAlt = {
|
||||||
|
alt,
|
||||||
|
lang,
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
title="Media description"
|
||||||
|
>
|
||||||
|
{dataAltLabel}
|
||||||
|
{!!index && <sup>{index}</sup>}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const MEDIA_CAPTION_LIMIT = 140;
|
||||||
|
export const isMediaCaptionLong = mem((caption) =>
|
||||||
|
caption?.length
|
||||||
|
? caption.length > MEDIA_CAPTION_LIMIT ||
|
||||||
|
/[\n\r].*[\n\r]/.test(caption.trim())
|
||||||
|
: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
function Media({
|
||||||
|
media,
|
||||||
|
to,
|
||||||
|
lang,
|
||||||
|
showOriginal,
|
||||||
|
autoAnimate,
|
||||||
|
showCaption,
|
||||||
|
altIndex,
|
||||||
|
onClick = () => {},
|
||||||
|
}) {
|
||||||
const {
|
const {
|
||||||
blurhash,
|
blurhash,
|
||||||
description,
|
description,
|
||||||
|
@ -134,6 +179,35 @@ function Media({ media, to, showOriginal, autoAnimate, onClick = () => {} }) {
|
||||||
aspectRatio: `${width} / ${height}`,
|
aspectRatio: `${width} / ${height}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const longDesc = isMediaCaptionLong(description);
|
||||||
|
const showInlineDesc =
|
||||||
|
!!showCaption && !showOriginal && !!description && !longDesc;
|
||||||
|
const Figure = !showInlineDesc
|
||||||
|
? Fragment
|
||||||
|
: (props) => {
|
||||||
|
const { children, ...restProps } = props;
|
||||||
|
return (
|
||||||
|
<figure {...restProps}>
|
||||||
|
{children}
|
||||||
|
<figcaption
|
||||||
|
class="media-caption"
|
||||||
|
lang={lang}
|
||||||
|
dir="auto"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
states.showMediaAlt = {
|
||||||
|
alt: description,
|
||||||
|
lang,
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{description}
|
||||||
|
</figcaption>
|
||||||
|
</figure>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
if (isImage) {
|
if (isImage) {
|
||||||
// Note: type: unknown might not have width/height
|
// Note: type: unknown might not have width/height
|
||||||
quickPinchZoomProps.containerProps.style.display = 'inherit';
|
quickPinchZoomProps.containerProps.style.display = 'inherit';
|
||||||
|
@ -152,79 +226,87 @@ function Media({ media, to, showOriginal, autoAnimate, onClick = () => {} }) {
|
||||||
}, [mediaURL]);
|
}, [mediaURL]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Parent
|
<Figure>
|
||||||
ref={parentRef}
|
<Parent
|
||||||
class={`media media-image`}
|
ref={parentRef}
|
||||||
onClick={onClick}
|
class={`media media-image`}
|
||||||
data-orientation={orientation}
|
onClick={onClick}
|
||||||
style={
|
data-orientation={orientation}
|
||||||
showOriginal
|
data-has-alt={!showInlineDesc}
|
||||||
? {
|
style={
|
||||||
backgroundImage: `url(${previewUrl})`,
|
showOriginal
|
||||||
backgroundSize: imageSmallerThanParent
|
? {
|
||||||
? `${width}px ${height}px`
|
backgroundImage: `url(${previewUrl})`,
|
||||||
: undefined,
|
backgroundSize: imageSmallerThanParent
|
||||||
}
|
? `${width}px ${height}px`
|
||||||
: mediaStyles
|
: undefined,
|
||||||
}
|
|
||||||
>
|
|
||||||
{showOriginal ? (
|
|
||||||
<QuickPinchZoom {...quickPinchZoomProps}>
|
|
||||||
<img
|
|
||||||
ref={mediaRef}
|
|
||||||
src={mediaURL}
|
|
||||||
alt={description}
|
|
||||||
width={width}
|
|
||||||
height={height}
|
|
||||||
data-orientation={orientation}
|
|
||||||
loading="eager"
|
|
||||||
decoding="sync"
|
|
||||||
onLoad={(e) => {
|
|
||||||
e.target.closest('.media-image').style.backgroundImage = '';
|
|
||||||
e.target.closest('.media-zoom').style.display = '';
|
|
||||||
setPinchZoomEnabled(true);
|
|
||||||
}}
|
|
||||||
onError={(e) => {
|
|
||||||
const { src } = e.target;
|
|
||||||
if (src === mediaURL) {
|
|
||||||
e.target.src = remoteMediaURL;
|
|
||||||
}
|
}
|
||||||
}}
|
: mediaStyles
|
||||||
/>
|
}
|
||||||
</QuickPinchZoom>
|
>
|
||||||
) : (
|
{showOriginal ? (
|
||||||
<img
|
<QuickPinchZoom {...quickPinchZoomProps}>
|
||||||
src={mediaURL}
|
<img
|
||||||
alt={description}
|
ref={mediaRef}
|
||||||
width={width}
|
src={mediaURL}
|
||||||
height={height}
|
alt={description}
|
||||||
data-orientation={orientation}
|
width={width}
|
||||||
loading="lazy"
|
height={height}
|
||||||
style={{
|
data-orientation={orientation}
|
||||||
backgroundColor:
|
loading="eager"
|
||||||
rgbAverageColor && `rgb(${rgbAverageColor.join(',')})`,
|
decoding="sync"
|
||||||
backgroundPosition: focalBackgroundPosition || 'center',
|
onLoad={(e) => {
|
||||||
// Duration based on width or height in pixels
|
e.target.closest('.media-image').style.backgroundImage = '';
|
||||||
// 100px per second (rough estimate)
|
e.target.closest('.media-zoom').style.display = '';
|
||||||
// Clamp between 5s and 120s
|
setPinchZoomEnabled(true);
|
||||||
'--anim-duration': `${Math.min(
|
}}
|
||||||
Math.max(Math.max(width, height) / 100, 5),
|
onError={(e) => {
|
||||||
120,
|
const { src } = e.target;
|
||||||
)}s`,
|
if (src === mediaURL) {
|
||||||
}}
|
e.target.src = remoteMediaURL;
|
||||||
onLoad={(e) => {
|
}
|
||||||
e.target.closest('.media-image').style.backgroundImage = '';
|
}}
|
||||||
e.target.dataset.loaded = true;
|
/>
|
||||||
}}
|
</QuickPinchZoom>
|
||||||
onError={(e) => {
|
) : (
|
||||||
const { src } = e.target;
|
<>
|
||||||
if (src === mediaURL) {
|
<img
|
||||||
e.target.src = remoteMediaURL;
|
src={mediaURL}
|
||||||
}
|
alt={showInlineDesc ? '' : description}
|
||||||
}}
|
width={width}
|
||||||
/>
|
height={height}
|
||||||
)}
|
data-orientation={orientation}
|
||||||
</Parent>
|
loading="lazy"
|
||||||
|
style={{
|
||||||
|
backgroundColor:
|
||||||
|
rgbAverageColor && `rgb(${rgbAverageColor.join(',')})`,
|
||||||
|
backgroundPosition: focalBackgroundPosition || 'center',
|
||||||
|
// Duration based on width or height in pixels
|
||||||
|
// 100px per second (rough estimate)
|
||||||
|
// Clamp between 5s and 120s
|
||||||
|
'--anim-duration': `${Math.min(
|
||||||
|
Math.max(Math.max(width, height) / 100, 5),
|
||||||
|
120,
|
||||||
|
)}s`,
|
||||||
|
}}
|
||||||
|
onLoad={(e) => {
|
||||||
|
e.target.closest('.media-image').style.backgroundImage = '';
|
||||||
|
e.target.dataset.loaded = true;
|
||||||
|
}}
|
||||||
|
onError={(e) => {
|
||||||
|
const { src } = e.target;
|
||||||
|
if (src === mediaURL) {
|
||||||
|
e.target.src = remoteMediaURL;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{!showInlineDesc && (
|
||||||
|
<AltBadge alt={description} lang={lang} index={altIndex} />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Parent>
|
||||||
|
</Figure>
|
||||||
);
|
);
|
||||||
} else if (type === 'gifv' || type === 'video' || isVideoMaybe) {
|
} else if (type === 'gifv' || type === 'video' || isVideoMaybe) {
|
||||||
const shortDuration = original.duration < 31;
|
const shortDuration = original.duration < 31;
|
||||||
|
@ -252,11 +334,8 @@ function Media({ media, to, showOriginal, autoAnimate, onClick = () => {} }) {
|
||||||
></video>
|
></video>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const showInlineDesc = !showOriginal && !isGIF && !!description;
|
|
||||||
const Container = showInlineDesc ? 'figure' : Fragment;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Figure>
|
||||||
<Parent
|
<Parent
|
||||||
class={`media media-${isGIF ? 'gif' : 'video'} ${
|
class={`media media-${isGIF ? 'gif' : 'video'} ${
|
||||||
autoGIFAnimate ? 'media-contain' : ''
|
autoGIFAnimate ? 'media-contain' : ''
|
||||||
|
@ -264,6 +343,7 @@ function Media({ media, to, showOriginal, autoAnimate, onClick = () => {} }) {
|
||||||
data-orientation={orientation}
|
data-orientation={orientation}
|
||||||
data-formatted-duration={formattedDuration}
|
data-formatted-duration={formattedDuration}
|
||||||
data-label={isGIF && !showOriginal && !autoGIFAnimate ? 'GIF' : ''}
|
data-label={isGIF && !showOriginal && !autoGIFAnimate ? 'GIF' : ''}
|
||||||
|
data-has-alt={!showInlineDesc}
|
||||||
// style={{
|
// style={{
|
||||||
// backgroundColor:
|
// backgroundColor:
|
||||||
// rgbAverageColor && `rgb(${rgbAverageColor.join(',')})`,
|
// rgbAverageColor && `rgb(${rgbAverageColor.join(',')})`,
|
||||||
|
@ -291,6 +371,20 @@ function Media({ media, to, showOriginal, autoAnimate, onClick = () => {} }) {
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
onFocus={() => {
|
||||||
|
if (hoverAnimate) {
|
||||||
|
try {
|
||||||
|
videoRef.current.play();
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onBlur={() => {
|
||||||
|
if (hoverAnimate) {
|
||||||
|
try {
|
||||||
|
videoRef.current.pause();
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{showOriginal || autoGIFAnimate ? (
|
{showOriginal || autoGIFAnimate ? (
|
||||||
isGIF && showOriginal ? (
|
isGIF && showOriginal ? (
|
||||||
|
@ -339,45 +433,47 @@ function Media({ media, to, showOriginal, autoAnimate, onClick = () => {} }) {
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
{!showOriginal && !showInlineDesc && (
|
||||||
|
<AltBadge alt={description} lang={lang} index={altIndex} />
|
||||||
|
)}
|
||||||
</Parent>
|
</Parent>
|
||||||
{showInlineDesc && (
|
</Figure>
|
||||||
<figcaption
|
|
||||||
onClick={() => {
|
|
||||||
location.hash = to;
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{description}
|
|
||||||
</figcaption>
|
|
||||||
)}
|
|
||||||
</Container>
|
|
||||||
);
|
);
|
||||||
} else if (type === 'audio') {
|
} else if (type === 'audio') {
|
||||||
const formattedDuration = formatDuration(original.duration);
|
const formattedDuration = formatDuration(original.duration);
|
||||||
return (
|
return (
|
||||||
<Parent
|
<Figure>
|
||||||
class="media media-audio"
|
<Parent
|
||||||
data-formatted-duration={formattedDuration}
|
class="media media-audio"
|
||||||
onClick={onClick}
|
data-formatted-duration={formattedDuration}
|
||||||
style={!showOriginal && mediaStyles}
|
data-has-alt={!showInlineDesc}
|
||||||
>
|
onClick={onClick}
|
||||||
{showOriginal ? (
|
style={!showOriginal && mediaStyles}
|
||||||
<audio src={remoteUrl || url} preload="none" controls autoplay />
|
>
|
||||||
) : previewUrl ? (
|
{showOriginal ? (
|
||||||
<img
|
<audio src={remoteUrl || url} preload="none" controls autoplay />
|
||||||
src={previewUrl}
|
) : previewUrl ? (
|
||||||
alt={description}
|
<img
|
||||||
width={width}
|
src={previewUrl}
|
||||||
height={height}
|
alt={showInlineDesc ? '' : description}
|
||||||
data-orientation={orientation}
|
width={width}
|
||||||
loading="lazy"
|
height={height}
|
||||||
/>
|
data-orientation={orientation}
|
||||||
) : null}
|
loading="lazy"
|
||||||
{!showOriginal && (
|
/>
|
||||||
<div class="media-play">
|
) : null}
|
||||||
<Icon icon="play" size="xl" />
|
{!showOriginal && (
|
||||||
</div>
|
<>
|
||||||
)}
|
<div class="media-play">
|
||||||
</Parent>
|
<Icon icon="play" size="xl" />
|
||||||
|
</div>
|
||||||
|
{!showInlineDesc && (
|
||||||
|
<AltBadge alt={description} lang={lang} index={altIndex} />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Parent>
|
||||||
|
</Figure>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ import AccountSheet from './account-sheet';
|
||||||
import Compose from './compose';
|
import Compose from './compose';
|
||||||
import Drafts from './drafts';
|
import Drafts from './drafts';
|
||||||
import GenericAccounts from './generic-accounts';
|
import GenericAccounts from './generic-accounts';
|
||||||
|
import MediaAltModal from './media-alt-modal';
|
||||||
import MediaModal from './media-modal';
|
import MediaModal from './media-modal';
|
||||||
import Modal from './modal';
|
import Modal from './modal';
|
||||||
import ShortcutsSettings from './shortcuts-settings';
|
import ShortcutsSettings from './shortcuts-settings';
|
||||||
|
@ -50,13 +51,17 @@ export default function Modals() {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
onClose={(results) => {
|
onClose={(results) => {
|
||||||
const { newStatus, instance } = results || {};
|
const { newStatus, instance, type } = results || {};
|
||||||
states.showCompose = false;
|
states.showCompose = false;
|
||||||
window.__COMPOSE__ = null;
|
window.__COMPOSE__ = null;
|
||||||
if (newStatus) {
|
if (newStatus) {
|
||||||
states.reloadStatusPage++;
|
states.reloadStatusPage++;
|
||||||
showToast({
|
showToast({
|
||||||
text: 'Post published. Check it out.',
|
text: {
|
||||||
|
post: 'Post published. Check it out.',
|
||||||
|
reply: 'Reply posted. Check it out.',
|
||||||
|
edit: 'Post updated. Check it out.',
|
||||||
|
}[type || 'post'],
|
||||||
delay: 1000,
|
delay: 1000,
|
||||||
duration: 10_000, // 10 seconds
|
duration: 10_000, // 10 seconds
|
||||||
onClick: (toast) => {
|
onClick: (toast) => {
|
||||||
|
@ -174,6 +179,24 @@ export default function Modals() {
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
)}
|
)}
|
||||||
|
{!!snapStates.showMediaAlt && (
|
||||||
|
<Modal
|
||||||
|
class="light"
|
||||||
|
onClick={(e) => {
|
||||||
|
if (e.target === e.currentTarget) {
|
||||||
|
states.showMediaAlt = false;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MediaAltModal
|
||||||
|
alt={snapStates.showMediaAlt.alt || snapStates.showMediaAlt}
|
||||||
|
lang={snapStates.showMediaAlt?.lang}
|
||||||
|
onClose={() => {
|
||||||
|
states.showMediaAlt = false;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,13 +25,20 @@ function NameText({
|
||||||
const trimmedDisplayName = (displayName || '').toLowerCase().trim();
|
const trimmedDisplayName = (displayName || '').toLowerCase().trim();
|
||||||
const shortenedDisplayName = trimmedDisplayName
|
const shortenedDisplayName = trimmedDisplayName
|
||||||
.replace(/(\:(\w|\+|\-)+\:)(?=|[\!\.\?]|$)/g, '') // Remove shortcodes, regex from https://regex101.com/r/iE9uV0/1
|
.replace(/(\:(\w|\+|\-)+\:)(?=|[\!\.\?]|$)/g, '') // Remove shortcodes, regex from https://regex101.com/r/iE9uV0/1
|
||||||
.replace(/\s+/g, '') // E.g. "My name" === "myname"
|
.replace(/\s+/g, ''); // E.g. "My name" === "myname"
|
||||||
.replace(/[^a-z0-9]/gi, ''); // Remove non-alphanumeric characters
|
const shortenedAlphaNumericDisplayName = shortenedDisplayName.replace(
|
||||||
|
/[^a-z0-9]/gi,
|
||||||
|
'',
|
||||||
|
); // Remove non-alphanumeric characters
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!short &&
|
!short &&
|
||||||
(trimmedUsername === trimmedDisplayName ||
|
(trimmedUsername === trimmedDisplayName ||
|
||||||
trimmedUsername === shortenedDisplayName)
|
trimmedUsername === shortenedDisplayName ||
|
||||||
|
trimmedUsername === shortenedAlphaNumericDisplayName ||
|
||||||
|
trimmedUsername.localeCompare?.(shortenedDisplayName, 'en', {
|
||||||
|
sensitivity: 'base',
|
||||||
|
}) === 0)
|
||||||
) {
|
) {
|
||||||
username = null;
|
username = null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,7 @@ import { accountsIsDtth, gtsDtthSettings } from '../utils/dtth';
|
||||||
|
|
||||||
function NavMenu(props) {
|
function NavMenu(props) {
|
||||||
const snapStates = useSnapshot(states);
|
const snapStates = useSnapshot(states);
|
||||||
const { instance, authenticated } = api();
|
const { masto, instance, authenticated } = api();
|
||||||
|
|
||||||
const [currentAccount, setCurrentAccount] = useState();
|
const [currentAccount, setCurrentAccount] = useState();
|
||||||
const [moreThanOneAccount, setMoreThanOneAccount] = useState(false);
|
const [moreThanOneAccount, setMoreThanOneAccount] = useState(false);
|
||||||
|
@ -61,6 +61,28 @@ function NavMenu(props) {
|
||||||
0,
|
0,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const mutesIterator = useRef();
|
||||||
|
async function fetchMutes(firstLoad) {
|
||||||
|
if (firstLoad || !mutesIterator.current) {
|
||||||
|
mutesIterator.current = masto.v1.mutes.list({
|
||||||
|
limit: 80,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const results = await mutesIterator.current.next();
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
const blocksIterator = useRef();
|
||||||
|
async function fetchBlocks(firstLoad) {
|
||||||
|
if (firstLoad || !blocksIterator.current) {
|
||||||
|
blocksIterator.current = masto.v1.blocks.list({
|
||||||
|
limit: 80,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const results = await blocksIterator.current.next();
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
|
@ -209,6 +231,29 @@ function NavMenu(props) {
|
||||||
>
|
>
|
||||||
<Icon icon="group" size="l" /> <span>Accounts…</span>
|
<Icon icon="group" size="l" /> <span>Accounts…</span>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
<MenuItem
|
||||||
|
onClick={() => {
|
||||||
|
states.showGenericAccounts = {
|
||||||
|
id: 'mute',
|
||||||
|
heading: 'Muted users',
|
||||||
|
fetchAccounts: fetchMutes,
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon icon="mute" size="l" /> Muted users…
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem
|
||||||
|
onClick={() => {
|
||||||
|
states.showGenericAccounts = {
|
||||||
|
id: 'block',
|
||||||
|
heading: 'Blocked users',
|
||||||
|
fetchAccounts: fetchBlocks,
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon icon="block" size="l" />
|
||||||
|
Blocked users…
|
||||||
|
</MenuItem>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
states.showKeyboardShortcutsHelp = true;
|
states.showKeyboardShortcutsHelp = true;
|
||||||
|
|
|
@ -38,7 +38,7 @@ export default memo(function NotificationService() {
|
||||||
? getAccountByAccessToken(accessToken)
|
? getAccountByAccessToken(accessToken)
|
||||||
: getCurrentAccount();
|
: getCurrentAccount();
|
||||||
(async () => {
|
(async () => {
|
||||||
const notification = await masto.v1.notifications.fetch(id);
|
const notification = await masto.v1.notifications.$select(id).fetch();
|
||||||
if (notification && account) {
|
if (notification && account) {
|
||||||
console.log('🛎️ Notification', { id, notification, account });
|
console.log('🛎️ Notification', { id, notification, account });
|
||||||
const accountInstance = account.instanceURL;
|
const accountInstance = account.instanceURL;
|
||||||
|
|
|
@ -58,14 +58,14 @@ const contentText = {
|
||||||
'favourite+reblog+account': (count) =>
|
'favourite+reblog+account': (count) =>
|
||||||
`boosted & favourited ${count} of your posts.`,
|
`boosted & favourited ${count} of your posts.`,
|
||||||
'favourite+reblog_reply': 'boosted & favourited your reply.',
|
'favourite+reblog_reply': 'boosted & favourited your reply.',
|
||||||
'admin.signup': 'signed up.',
|
'admin.sign_up': 'signed up.',
|
||||||
'admin.report': 'reported a post.',
|
'admin.report': (targetAccount) => <>reported {targetAccount}</>,
|
||||||
};
|
};
|
||||||
|
|
||||||
const AVATARS_LIMIT = 50;
|
const AVATARS_LIMIT = 50;
|
||||||
|
|
||||||
function Notification({ notification, instance, reload, isStatic }) {
|
function Notification({ notification, instance, reload, isStatic }) {
|
||||||
const { id, status, account, _accounts, _statuses } = notification;
|
const { id, status, account, report, _accounts, _statuses } = notification;
|
||||||
let { type } = notification;
|
let { type } = notification;
|
||||||
|
|
||||||
// status = Attached when type of the notification is favourite, reblog, status, mention, poll, or update
|
// status = Attached when type of the notification is favourite, reblog, status, mention, poll, or update
|
||||||
|
@ -119,7 +119,15 @@ function Notification({ notification, instance, reload, isStatic }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof text === 'function') {
|
if (typeof text === 'function') {
|
||||||
text = text(_statuses?.length || _accounts?.length);
|
const count = _statuses?.length || _accounts?.length;
|
||||||
|
if (count) {
|
||||||
|
text = text(count);
|
||||||
|
} else if (type === 'admin.report') {
|
||||||
|
const targetAccount = report?.targetAccount;
|
||||||
|
if (targetAccount) {
|
||||||
|
text = text(<NameText account={targetAccount} showAvatar />);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === 'mention' && !status) {
|
if (type === 'mention' && !status) {
|
||||||
|
|
|
@ -199,6 +199,7 @@ export default function Poll({
|
||||||
setUIState('default');
|
setUIState('default');
|
||||||
})();
|
})();
|
||||||
}}
|
}}
|
||||||
|
title="Refresh"
|
||||||
>
|
>
|
||||||
<Icon icon="refresh" alt="Refresh" />
|
<Icon icon="refresh" alt="Refresh" />
|
||||||
</button>
|
</button>
|
||||||
|
@ -212,6 +213,7 @@ export default function Poll({
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setShowResults(!showResults);
|
setShowResults(!showResults);
|
||||||
}}
|
}}
|
||||||
|
title={showResults ? 'Hide results' : 'Show results'}
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
icon={showResults ? 'eye-open' : 'eye-close'}
|
icon={showResults ? 'eye-open' : 'eye-close'}
|
||||||
|
|
|
@ -4,14 +4,15 @@ import {
|
||||||
compressToEncodedURIComponent,
|
compressToEncodedURIComponent,
|
||||||
decompressFromEncodedURIComponent,
|
decompressFromEncodedURIComponent,
|
||||||
} from 'lz-string';
|
} from 'lz-string';
|
||||||
import mem from 'mem';
|
|
||||||
import { useEffect, useMemo, useRef, useState } from 'preact/hooks';
|
import { useEffect, useMemo, useRef, useState } from 'preact/hooks';
|
||||||
import { useSnapshot } from 'valtio';
|
import { useSnapshot } from 'valtio';
|
||||||
|
|
||||||
import floatingButtonUrl from '../assets/floating-button.svg';
|
import floatingButtonUrl from '../assets/floating-button.svg';
|
||||||
import multiColumnUrl from '../assets/multi-column.svg';
|
import multiColumnUrl from '../assets/multi-column.svg';
|
||||||
import tabMenuBarUrl from '../assets/tab-menu-bar.svg';
|
import tabMenuBarUrl from '../assets/tab-menu-bar.svg';
|
||||||
|
|
||||||
import { api } from '../utils/api';
|
import { api } from '../utils/api';
|
||||||
|
import pmem from '../utils/pmem';
|
||||||
import showToast from '../utils/show-toast';
|
import showToast from '../utils/show-toast';
|
||||||
import states from '../utils/states';
|
import states from '../utils/states';
|
||||||
|
|
||||||
|
@ -132,15 +133,10 @@ export const SHORTCUTS_META = {
|
||||||
},
|
},
|
||||||
list: {
|
list: {
|
||||||
id: 'list',
|
id: 'list',
|
||||||
title: mem(
|
title: pmem(async ({ id }) => {
|
||||||
async ({ id }) => {
|
const list = await api().masto.v1.lists.$select(id).fetch();
|
||||||
const list = await api().masto.v1.lists.fetch(id);
|
return list.title;
|
||||||
return list.title;
|
}),
|
||||||
},
|
|
||||||
{
|
|
||||||
cacheKey: ([{ id }]) => id,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
path: ({ id }) => `/l/${id}`,
|
path: ({ id }) => `/l/${id}`,
|
||||||
icon: 'list',
|
icon: 'list',
|
||||||
},
|
},
|
||||||
|
@ -166,15 +162,10 @@ export const SHORTCUTS_META = {
|
||||||
},
|
},
|
||||||
'account-statuses': {
|
'account-statuses': {
|
||||||
id: 'account-statuses',
|
id: 'account-statuses',
|
||||||
title: mem(
|
title: pmem(async ({ id }) => {
|
||||||
async ({ id }) => {
|
const account = await api().masto.v1.accounts.$select(id).fetch();
|
||||||
const account = await api().masto.v1.accounts.fetch(id);
|
return account.username || account.acct || account.displayName;
|
||||||
return account.username || account.acct || account.displayName;
|
}),
|
||||||
},
|
|
||||||
{
|
|
||||||
cacheKey: ([{ id }]) => id,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
path: ({ id }) => `/a/${id}`,
|
path: ({ id }) => `/a/${id}`,
|
||||||
icon: 'user',
|
icon: 'user',
|
||||||
},
|
},
|
||||||
|
|
|
@ -82,6 +82,8 @@
|
||||||
list-style: none;
|
list-style: none;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
min-width: 20vw;
|
||||||
|
flex-basis: 20vw;
|
||||||
}
|
}
|
||||||
#shortcuts .tab-bar li a {
|
#shortcuts .tab-bar li a {
|
||||||
-webkit-tap-highlight-color: transparent;
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
@ -95,7 +97,13 @@
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
text-shadow: 0 var(--hairline-width) var(--bg-color);
|
text-shadow: 0 var(--hairline-width) var(--bg-color);
|
||||||
width: 20vw;
|
width: 100%;
|
||||||
|
|
||||||
|
@media (hover: hover) {
|
||||||
|
&:is(:hover, :focus) {
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
#shortcuts .tab-bar li a:active {
|
#shortcuts .tab-bar li a:active {
|
||||||
transform: scale(0.95);
|
transform: scale(0.95);
|
||||||
|
@ -171,6 +179,8 @@ shortcuts .tab-bar[hidden] {
|
||||||
}
|
}
|
||||||
#shortcuts .tab-bar li {
|
#shortcuts .tab-bar li {
|
||||||
flex-grow: 0;
|
flex-grow: 0;
|
||||||
|
min-width: auto;
|
||||||
|
flex-basis: auto;
|
||||||
}
|
}
|
||||||
#shortcuts .tab-bar li a {
|
#shortcuts .tab-bar li a {
|
||||||
padding: 0 16px;
|
padding: 0 16px;
|
||||||
|
|
|
@ -166,8 +166,11 @@
|
||||||
.status.large .status-card :is(.content, .poll, .media-container) {
|
.status.large .status-card :is(.content, .poll, .media-container) {
|
||||||
max-height: 80vh !important;
|
max-height: 80vh !important;
|
||||||
}
|
}
|
||||||
.status-card :is(.content.truncated, .poll, .media-container.truncated) {
|
.status-card :is(.content, .poll, .media-container) {
|
||||||
font-size: inherit !important;
|
font-size: inherit !important;
|
||||||
|
}
|
||||||
|
.status-card :is(.content.truncated, .poll, .media-container.truncated) {
|
||||||
|
/* font-size: inherit !important; */
|
||||||
mask-image: linear-gradient(to bottom, #000 80px, transparent);
|
mask-image: linear-gradient(to bottom, #000 80px, transparent);
|
||||||
}
|
}
|
||||||
.status.small
|
.status.small
|
||||||
|
@ -299,7 +302,7 @@
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
/* text-overflow: ellipsis; */
|
/* text-overflow: ellipsis; */
|
||||||
}
|
}
|
||||||
.status > .container > .meta .name-text {
|
.status > .container > .meta .meta-name {
|
||||||
mask-image: linear-gradient(to left, transparent, black 16px);
|
mask-image: linear-gradient(to left, transparent, black 16px);
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
@ -334,7 +337,7 @@
|
||||||
.status > .container > .meta a.time:after {
|
.status > .container > .meta a.time:after {
|
||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: -16px;
|
inset: -16px -16px -8px;
|
||||||
}
|
}
|
||||||
.status > .container > .meta .reply-to {
|
.status > .container > .meta .reply-to {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
|
@ -457,7 +460,7 @@
|
||||||
.status
|
.status
|
||||||
.content-container.has-spoiler:not(.show-spoiler)
|
.content-container.has-spoiler:not(.show-spoiler)
|
||||||
.spoiler
|
.spoiler
|
||||||
~ *:not(.media-container, .card),
|
~ *:not(.media-container, .card, .media-figure-multiple),
|
||||||
.status
|
.status
|
||||||
.content-container.has-spoiler:not(.show-spoiler)
|
.content-container.has-spoiler:not(.show-spoiler)
|
||||||
.spoiler
|
.spoiler
|
||||||
|
@ -466,7 +469,7 @@
|
||||||
.status
|
.status
|
||||||
.content-container.has-spoiler:not(.show-spoiler)
|
.content-container.has-spoiler:not(.show-spoiler)
|
||||||
.spoiler
|
.spoiler
|
||||||
~ .media-container
|
~ :is(.media-container, .media-figure-multiple)
|
||||||
figcaption {
|
figcaption {
|
||||||
filter: blur(5px) invert(0.5);
|
filter: blur(5px) invert(0.5);
|
||||||
image-rendering: crisp-edges;
|
image-rendering: crisp-edges;
|
||||||
|
@ -480,7 +483,7 @@
|
||||||
.status
|
.status
|
||||||
.content-container.has-spoiler:not(.show-spoiler)
|
.content-container.has-spoiler:not(.show-spoiler)
|
||||||
.spoiler
|
.spoiler
|
||||||
~ .media-container
|
~ :is(.media-container, .media-figure-multiple)
|
||||||
.media
|
.media
|
||||||
> *,
|
> *,
|
||||||
.status
|
.status
|
||||||
|
@ -544,7 +547,7 @@
|
||||||
max-height: 40vh;
|
max-height: 40vh;
|
||||||
max-height: 40dvh;
|
max-height: 40dvh;
|
||||||
}
|
}
|
||||||
.timeline-deck .status .content.truncated {
|
.timeline-deck .status:not(.truncated .status) .content.truncated {
|
||||||
mask-image: linear-gradient(
|
mask-image: linear-gradient(
|
||||||
to top,
|
to top,
|
||||||
transparent,
|
transparent,
|
||||||
|
@ -552,7 +555,7 @@
|
||||||
black 1.5em
|
black 1.5em
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
.timeline-deck .status .content.truncated:after {
|
.timeline-deck .status:not(.truncated .status) .content.truncated:after {
|
||||||
content: attr(data-read-more);
|
content: attr(data-read-more);
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
@ -708,21 +711,21 @@
|
||||||
figure {
|
figure {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
/* align-items: flex-end; */
|
||||||
|
column-gap: 4px;
|
||||||
|
|
||||||
figcaption {
|
figcaption {
|
||||||
margin: -2px 0 0;
|
align-self: flex-end;
|
||||||
padding: 0 4px;
|
padding: 4px;
|
||||||
font-size: 90%;
|
font-size: 90%;
|
||||||
color: var(--text-insignificant-color);
|
color: var(--text-insignificant-color);
|
||||||
overflow: hidden;
|
|
||||||
white-space: normal;
|
|
||||||
display: -webkit-box;
|
|
||||||
display: box;
|
|
||||||
-webkit-box-orient: vertical;
|
|
||||||
box-orient: vertical;
|
|
||||||
-webkit-line-clamp: 2;
|
|
||||||
line-clamp: 2;
|
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
|
cursor: pointer;
|
||||||
|
white-space: pre-line;
|
||||||
|
flex-basis: 15em;
|
||||||
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -833,7 +836,7 @@
|
||||||
.status .media:is(:hover, :focus) {
|
.status .media:is(:hover, :focus) {
|
||||||
border-color: var(--outline-hover-color);
|
border-color: var(--outline-hover-color);
|
||||||
}
|
}
|
||||||
.status .media:active {
|
.status .media:active:not(:has(button:active)) {
|
||||||
filter: brightness(0.8);
|
filter: brightness(0.8);
|
||||||
transform: scale(0.99);
|
transform: scale(0.99);
|
||||||
}
|
}
|
||||||
|
@ -845,6 +848,22 @@
|
||||||
}
|
}
|
||||||
.status .media {
|
.status .media {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
|
&[data-has-alt] {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.alt-badge {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 8px;
|
||||||
|
left: 8px;
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: -12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.status .media img:is(:hover, :focus),
|
.status .media img:is(:hover, :focus),
|
||||||
a:focus-visible .status .media img {
|
a:focus-visible .status .media img {
|
||||||
|
@ -874,9 +893,9 @@ body:has(#modal-container .carousel) .status .media img:hover {
|
||||||
left: 50%;
|
left: 50%;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
transform: translate(-50%, -50%);
|
transform: translate(-50%, -50%);
|
||||||
color: var(--video-fg-color);
|
color: var(--media-fg-color);
|
||||||
background-color: var(--video-bg-color);
|
background-color: var(--media-bg-color);
|
||||||
box-shadow: inset 0 0 0 2px var(--video-outline-color);
|
box-shadow: inset 0 0 0 2px var(--media-outline-color);
|
||||||
display: flex;
|
display: flex;
|
||||||
place-content: center;
|
place-content: center;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
|
@ -893,9 +912,9 @@ body:has(#modal-container .carousel) .status .media img:hover {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 8px;
|
bottom: 8px;
|
||||||
right: 8px;
|
right: 8px;
|
||||||
color: var(--video-fg-color);
|
color: var(--media-fg-color);
|
||||||
background-color: var(--video-bg-color);
|
background-color: var(--media-bg-color);
|
||||||
border: var(--hairline-width) solid var(--video-outline-color);
|
border: var(--hairline-width) solid var(--media-outline-color);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
padding: 0 4px;
|
padding: 0 4px;
|
||||||
}
|
}
|
||||||
|
@ -910,9 +929,9 @@ body:has(#modal-container .carousel) .status .media img:hover {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 8px;
|
bottom: 8px;
|
||||||
right: 8px;
|
right: 8px;
|
||||||
color: var(--bg-faded-color);
|
color: var(--media-fg-color);
|
||||||
background-color: var(--text-insignificant-color);
|
background-color: var(--media-bg-color);
|
||||||
backdrop-filter: blur(6px) saturate(3) invert(0.2);
|
border: var(--hairline-width) solid var(--media-outline-color);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
padding: 0 4px;
|
padding: 0 4px;
|
||||||
}
|
}
|
||||||
|
@ -979,6 +998,62 @@ body:has(#modal-container .carousel) .status .media img:hover {
|
||||||
white-space: normal;
|
white-space: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.media-figure-multiple {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
figcaption {
|
||||||
|
padding: 4px;
|
||||||
|
font-size: 90%;
|
||||||
|
color: var(--text-insignificant-color);
|
||||||
|
line-height: 1.2;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
|
||||||
|
& > * {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
display: inline-flex;
|
||||||
|
gap: 4px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--text-color);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:only-child {
|
||||||
|
white-space: pre-line;
|
||||||
|
overflow: auto;
|
||||||
|
text-overflow: unset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sup {
|
||||||
|
opacity: 0.75;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Only 4, for now. Would be better if this is a for loop */
|
||||||
|
&:has(.media[data-has-alt]:nth-child(1):is(:hover, :focus))
|
||||||
|
figcaption
|
||||||
|
> div[data-caption-index~='1'],
|
||||||
|
&:has(.media[data-has-alt]:nth-child(2):is(:hover, :focus))
|
||||||
|
figcaption
|
||||||
|
> div[data-caption-index~='2'],
|
||||||
|
&:has(.media[data-has-alt]:nth-child(3):is(:hover, :focus))
|
||||||
|
figcaption
|
||||||
|
> div[data-caption-index~='3'],
|
||||||
|
&:has(.media[data-has-alt]:nth-child(4):is(:hover, :focus))
|
||||||
|
figcaption
|
||||||
|
> div[data-caption-index~='4'] {
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.carousel-item {
|
.carousel-item {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
@ -1003,6 +1078,12 @@ body:has(#modal-container .carousel) .status .media img:hover {
|
||||||
font-size: 90%;
|
font-size: 90%;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
text-shadow: 0 var(--hairline-width) var(--bg-color);
|
text-shadow: 0 var(--hairline-width) var(--bg-color);
|
||||||
|
mix-blend-mode: luminosity;
|
||||||
|
white-space: pre-line;
|
||||||
|
|
||||||
|
&:is(:hover, :focus) {
|
||||||
|
mix-blend-mode: normal;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.carousel-item button.media-alt .media-alt-desc {
|
.carousel-item button.media-alt .media-alt-desc {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
@ -1638,3 +1719,37 @@ a.card:is(:hover, :focus):visited {
|
||||||
#reactions-container .reactions-block .reblog-icon {
|
#reactions-container .reactions-block .reblog-icon {
|
||||||
color: var(--reblog-color);
|
color: var(--reblog-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ALT BADGE */
|
||||||
|
|
||||||
|
.alt-badge {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: var(--media-fg-color);
|
||||||
|
background-color: var(--media-bg-color);
|
||||||
|
border: var(--hairline-width) solid var(--media-outline-color);
|
||||||
|
mix-blend-mode: luminosity;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 4px;
|
||||||
|
opacity: 0.65;
|
||||||
|
|
||||||
|
sup {
|
||||||
|
vertical-align: super;
|
||||||
|
font-weight: normal;
|
||||||
|
line-height: 0;
|
||||||
|
padding-left: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.clickable {
|
||||||
|
opacity: 0.75;
|
||||||
|
border-width: 2px;
|
||||||
|
|
||||||
|
&:is(:hover, :focus):not(:active) {
|
||||||
|
transition: 0.15s ease-out;
|
||||||
|
transition-property: transform, opacity, mix-blend-mode;
|
||||||
|
transform: scale(1.15);
|
||||||
|
opacity: 0.9;
|
||||||
|
mix-blend-mode: normal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -9,7 +9,6 @@ import {
|
||||||
MenuItem,
|
MenuItem,
|
||||||
} from '@szhsin/react-menu';
|
} from '@szhsin/react-menu';
|
||||||
import { decodeBlurHash } from 'fast-blurhash';
|
import { decodeBlurHash } from 'fast-blurhash';
|
||||||
import mem from 'mem';
|
|
||||||
import pThrottle from 'p-throttle';
|
import pThrottle from 'p-throttle';
|
||||||
import { memo } from 'preact/compat';
|
import { memo } from 'preact/compat';
|
||||||
import {
|
import {
|
||||||
|
@ -22,7 +21,6 @@ import {
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
import { InView } from 'react-intersection-observer';
|
import { InView } from 'react-intersection-observer';
|
||||||
import { useLongPress } from 'use-long-press';
|
import { useLongPress } from 'use-long-press';
|
||||||
import useResizeObserver from 'use-resize-observer';
|
|
||||||
import { useSnapshot } from 'valtio';
|
import { useSnapshot } from 'valtio';
|
||||||
import { snapshot } from 'valtio/vanilla';
|
import { snapshot } from 'valtio/vanilla';
|
||||||
|
|
||||||
|
@ -43,6 +41,7 @@ import htmlContentLength from '../utils/html-content-length';
|
||||||
import isMastodonLinkMaybe from '../utils/isMastodonLinkMaybe';
|
import isMastodonLinkMaybe from '../utils/isMastodonLinkMaybe';
|
||||||
import localeMatch from '../utils/locale-match';
|
import localeMatch from '../utils/locale-match';
|
||||||
import niceDateTime from '../utils/nice-date-time';
|
import niceDateTime from '../utils/nice-date-time';
|
||||||
|
import pmem from '../utils/pmem';
|
||||||
import safeBoundingBoxPadding from '../utils/safe-bounding-box-padding';
|
import safeBoundingBoxPadding from '../utils/safe-bounding-box-padding';
|
||||||
import shortenNumber from '../utils/shorten-number';
|
import shortenNumber from '../utils/shorten-number';
|
||||||
import showToast from '../utils/show-toast';
|
import showToast from '../utils/show-toast';
|
||||||
|
@ -56,6 +55,7 @@ import Avatar from './avatar';
|
||||||
import Icon from './icon';
|
import Icon from './icon';
|
||||||
import Link from './link';
|
import Link from './link';
|
||||||
import Media from './media';
|
import Media from './media';
|
||||||
|
import { isMediaCaptionLong } from './media';
|
||||||
import MenuLink from './menu-link';
|
import MenuLink from './menu-link';
|
||||||
import RelativeTime from './relative-time';
|
import RelativeTime from './relative-time';
|
||||||
import TranslationBlock from './translation-block';
|
import TranslationBlock from './translation-block';
|
||||||
|
@ -67,13 +67,9 @@ const throttle = pThrottle({
|
||||||
});
|
});
|
||||||
|
|
||||||
function fetchAccount(id, masto) {
|
function fetchAccount(id, masto) {
|
||||||
try {
|
return masto.v1.accounts.$select(id).fetch();
|
||||||
return masto.v1.accounts.fetch(id);
|
|
||||||
} catch (e) {
|
|
||||||
return Promise.reject(e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
const memFetchAccount = mem(fetchAccount);
|
const memFetchAccount = pmem(fetchAccount);
|
||||||
|
|
||||||
const visibilityText = {
|
const visibilityText = {
|
||||||
public: 'Public',
|
public: 'Public',
|
||||||
|
@ -390,11 +386,11 @@ function Status({
|
||||||
reblogsCount: reblogsCount + (reblogged ? -1 : 1),
|
reblogsCount: reblogsCount + (reblogged ? -1 : 1),
|
||||||
};
|
};
|
||||||
if (reblogged) {
|
if (reblogged) {
|
||||||
const newStatus = await masto.v1.statuses.unreblog(id);
|
const newStatus = await masto.v1.statuses.$select(id).unreblog();
|
||||||
saveStatus(newStatus, instance);
|
saveStatus(newStatus, instance);
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
const newStatus = await masto.v1.statuses.reblog(id);
|
const newStatus = await masto.v1.statuses.$select(id).reblog();
|
||||||
saveStatus(newStatus, instance);
|
saveStatus(newStatus, instance);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -418,11 +414,11 @@ function Status({
|
||||||
reblogsCount: reblogsCount + (reblogged ? -1 : 1),
|
reblogsCount: reblogsCount + (reblogged ? -1 : 1),
|
||||||
};
|
};
|
||||||
if (reblogged) {
|
if (reblogged) {
|
||||||
const newStatus = await masto.v1.statuses.unreblog(id);
|
const newStatus = await masto.v1.statuses.$select(id).unreblog();
|
||||||
saveStatus(newStatus, instance);
|
saveStatus(newStatus, instance);
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
const newStatus = await masto.v1.statuses.reblog(id);
|
const newStatus = await masto.v1.statuses.$select(id).reblog();
|
||||||
saveStatus(newStatus, instance);
|
saveStatus(newStatus, instance);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -446,10 +442,10 @@ function Status({
|
||||||
favouritesCount: favouritesCount + (favourited ? -1 : 1),
|
favouritesCount: favouritesCount + (favourited ? -1 : 1),
|
||||||
};
|
};
|
||||||
if (favourited) {
|
if (favourited) {
|
||||||
const newStatus = await masto.v1.statuses.unfavourite(id);
|
const newStatus = await masto.v1.statuses.$select(id).unfavourite();
|
||||||
saveStatus(newStatus, instance);
|
saveStatus(newStatus, instance);
|
||||||
} else {
|
} else {
|
||||||
const newStatus = await masto.v1.statuses.favourite(id);
|
const newStatus = await masto.v1.statuses.$select(id).favourite();
|
||||||
saveStatus(newStatus, instance);
|
saveStatus(newStatus, instance);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -470,10 +466,10 @@ function Status({
|
||||||
bookmarked: !bookmarked,
|
bookmarked: !bookmarked,
|
||||||
};
|
};
|
||||||
if (bookmarked) {
|
if (bookmarked) {
|
||||||
const newStatus = await masto.v1.statuses.unbookmark(id);
|
const newStatus = await masto.v1.statuses.$select(id).unbookmark();
|
||||||
saveStatus(newStatus, instance);
|
saveStatus(newStatus, instance);
|
||||||
} else {
|
} else {
|
||||||
const newStatus = await masto.v1.statuses.bookmark(id);
|
const newStatus = await masto.v1.statuses.$select(id).bookmark();
|
||||||
saveStatus(newStatus, instance);
|
saveStatus(newStatus, instance);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -484,7 +480,7 @@ function Status({
|
||||||
};
|
};
|
||||||
|
|
||||||
const differentLanguage =
|
const differentLanguage =
|
||||||
language &&
|
!!language &&
|
||||||
language !== targetLanguage &&
|
language !== targetLanguage &&
|
||||||
!localeMatch([language], [targetLanguage]) &&
|
!localeMatch([language], [targetLanguage]) &&
|
||||||
!contentTranslationHideLanguages.find(
|
!contentTranslationHideLanguages.find(
|
||||||
|
@ -708,9 +704,9 @@ function Status({
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
try {
|
try {
|
||||||
const newStatus = await masto.v1.statuses[
|
const newStatus = await masto.v1.statuses
|
||||||
muted ? 'unmute' : 'mute'
|
.$select(id)
|
||||||
](id);
|
[muted ? 'unmute' : 'mute']();
|
||||||
saveStatus(newStatus, instance);
|
saveStatus(newStatus, instance);
|
||||||
showToast(muted ? 'Conversation unmuted' : 'Conversation muted');
|
showToast(muted ? 'Conversation unmuted' : 'Conversation muted');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -763,7 +759,7 @@ function Status({
|
||||||
// if (yes) {
|
// if (yes) {
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
await masto.v1.statuses.remove(id);
|
await masto.v1.statuses.$select(id).remove();
|
||||||
const cachedStatus = getStatus(id, instance);
|
const cachedStatus = getStatus(id, instance);
|
||||||
cachedStatus._deleted = true;
|
cachedStatus._deleted = true;
|
||||||
showToast('Deleted');
|
showToast('Deleted');
|
||||||
|
@ -790,24 +786,34 @@ function Status({
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 0,
|
y: 0,
|
||||||
});
|
});
|
||||||
|
const isIOS =
|
||||||
|
window.ontouchstart !== undefined &&
|
||||||
|
/iPad|iPhone|iPod/.test(navigator.userAgent);
|
||||||
|
// Only iOS/iPadOS browsers don't support contextmenu
|
||||||
|
// Some comments report iPadOS might support contextmenu if a mouse is connected
|
||||||
const bindLongPressContext = useLongPress(
|
const bindLongPressContext = useLongPress(
|
||||||
(e) => {
|
isIOS
|
||||||
const { clientX, clientY } = e.touches?.[0] || e;
|
? (e) => {
|
||||||
// link detection copied from onContextMenu because here it works
|
if (e.pointerType === 'mouse') return;
|
||||||
const link = e.target.closest('a');
|
// There's 'pen' too, but not sure if contextmenu event would trigger from a pen
|
||||||
if (link && /^https?:\/\//.test(link.getAttribute('href'))) return;
|
|
||||||
e.preventDefault();
|
const { clientX, clientY } = e.touches?.[0] || e;
|
||||||
setContextMenuAnchorPoint({
|
// link detection copied from onContextMenu because here it works
|
||||||
x: clientX,
|
const link = e.target.closest('a');
|
||||||
y: clientY,
|
if (link && /^https?:\/\//.test(link.getAttribute('href'))) return;
|
||||||
});
|
e.preventDefault();
|
||||||
setIsContextMenuOpen(true);
|
setContextMenuAnchorPoint({
|
||||||
},
|
x: clientX,
|
||||||
|
y: clientY,
|
||||||
|
});
|
||||||
|
setIsContextMenuOpen(true);
|
||||||
|
}
|
||||||
|
: null,
|
||||||
{
|
{
|
||||||
threshold: 600,
|
threshold: 600,
|
||||||
captureEvent: true,
|
captureEvent: true,
|
||||||
detect: 'touch',
|
detect: 'touch',
|
||||||
cancelOnMovement: 4, // true allows movement of up to 25 pixels
|
cancelOnMovement: 2, // true allows movement of up to 25 pixels
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -862,6 +868,72 @@ function Status({
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const displayedMediaAttachments = mediaAttachments.slice(
|
||||||
|
0,
|
||||||
|
isSizeLarge ? undefined : 4,
|
||||||
|
);
|
||||||
|
const showMultipleMediaCaptions =
|
||||||
|
mediaAttachments.length > 1 &&
|
||||||
|
displayedMediaAttachments.some(
|
||||||
|
(media) => !!media.description && !isMediaCaptionLong(media.description),
|
||||||
|
);
|
||||||
|
const captionChildren = useMemo(() => {
|
||||||
|
if (!showMultipleMediaCaptions) return null;
|
||||||
|
const attachments = [];
|
||||||
|
displayedMediaAttachments.forEach((media, i) => {
|
||||||
|
if (!media.description) return;
|
||||||
|
const index = attachments.findIndex(
|
||||||
|
(attachment) => attachment.media.description === media.description,
|
||||||
|
);
|
||||||
|
if (index === -1) {
|
||||||
|
attachments.push({
|
||||||
|
media,
|
||||||
|
indices: [i],
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
attachments[index].indices.push(i);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return attachments.map(({ media, indices }) => (
|
||||||
|
<div
|
||||||
|
key={media.id}
|
||||||
|
data-caption-index={indices.map((i) => i + 1).join(' ')}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
states.showMediaAlt = {
|
||||||
|
alt: media.description,
|
||||||
|
lang: language,
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
title={media.description}
|
||||||
|
>
|
||||||
|
<sup>{indices.map((i) => i + 1).join(' ')}</sup> {media.description}
|
||||||
|
</div>
|
||||||
|
));
|
||||||
|
|
||||||
|
// return displayedMediaAttachments.map(
|
||||||
|
// (media, i) =>
|
||||||
|
// !!media.description && (
|
||||||
|
// <div
|
||||||
|
// key={media.id}
|
||||||
|
// data-caption-index={i + 1}
|
||||||
|
// onClick={(e) => {
|
||||||
|
// e.preventDefault();
|
||||||
|
// e.stopPropagation();
|
||||||
|
// states.showMediaAlt = {
|
||||||
|
// alt: media.description,
|
||||||
|
// lang: language,
|
||||||
|
// };
|
||||||
|
// }}
|
||||||
|
// title={media.description}
|
||||||
|
// >
|
||||||
|
// <sup>{i + 1}</sup> {media.description}
|
||||||
|
// </div>
|
||||||
|
// ),
|
||||||
|
// );
|
||||||
|
}, [showMultipleMediaCaptions, displayedMediaAttachments, language]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<article
|
<article
|
||||||
ref={(node) => {
|
ref={(node) => {
|
||||||
|
@ -968,13 +1040,14 @@ function Status({
|
||||||
)}
|
)}
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="meta">
|
<div class="meta">
|
||||||
{/* <span> */}
|
<span class="meta-name">
|
||||||
<NameText
|
<NameText
|
||||||
account={status.account}
|
account={status.account}
|
||||||
instance={instance}
|
instance={instance}
|
||||||
showAvatar={size === 's'}
|
showAvatar={size === 's'}
|
||||||
showAcct={isSizeLarge}
|
showAcct={isSizeLarge}
|
||||||
/>
|
/>
|
||||||
|
</span>
|
||||||
{/* {inReplyToAccount && !withinContext && size !== 's' && (
|
{/* {inReplyToAccount && !withinContext && size !== 's' && (
|
||||||
<>
|
<>
|
||||||
{' '}
|
{' '}
|
||||||
|
@ -1191,7 +1264,8 @@ function Status({
|
||||||
}}
|
}}
|
||||||
refresh={() => {
|
refresh={() => {
|
||||||
return masto.v1.polls
|
return masto.v1.polls
|
||||||
.fetch(poll.id)
|
.$select(poll.id)
|
||||||
|
.fetch()
|
||||||
.then((pollResponse) => {
|
.then((pollResponse) => {
|
||||||
states.statuses[sKey].poll = pollResponse;
|
states.statuses[sKey].poll = pollResponse;
|
||||||
})
|
})
|
||||||
|
@ -1199,7 +1273,8 @@ function Status({
|
||||||
}}
|
}}
|
||||||
votePoll={(choices) => {
|
votePoll={(choices) => {
|
||||||
return masto.v1.polls
|
return masto.v1.polls
|
||||||
.vote(poll.id, {
|
.$select(poll.id)
|
||||||
|
.votes.create({
|
||||||
choices,
|
choices,
|
||||||
})
|
})
|
||||||
.then((pollResponse) => {
|
.then((pollResponse) => {
|
||||||
|
@ -1255,19 +1330,27 @@ function Status({
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{!!mediaAttachments.length && (
|
{!!mediaAttachments.length && (
|
||||||
<div
|
<MultipleMediaFigure
|
||||||
ref={mediaContainerRef}
|
lang={language}
|
||||||
class={`media-container media-eq${mediaAttachments.length} ${
|
enabled={showMultipleMediaCaptions}
|
||||||
mediaAttachments.length > 2 ? 'media-gt2' : ''
|
captionChildren={captionChildren}
|
||||||
} ${mediaAttachments.length > 4 ? 'media-gt4' : ''}`}
|
|
||||||
>
|
>
|
||||||
{mediaAttachments
|
<div
|
||||||
.slice(0, isSizeLarge ? undefined : 4)
|
ref={mediaContainerRef}
|
||||||
.map((media, i) => (
|
class={`media-container media-eq${mediaAttachments.length} ${
|
||||||
|
mediaAttachments.length > 2 ? 'media-gt2' : ''
|
||||||
|
} ${mediaAttachments.length > 4 ? 'media-gt4' : ''}`}
|
||||||
|
>
|
||||||
|
{displayedMediaAttachments.map((media, i) => (
|
||||||
<Media
|
<Media
|
||||||
key={media.id}
|
key={media.id}
|
||||||
media={media}
|
media={media}
|
||||||
autoAnimate={isSizeLarge}
|
autoAnimate={isSizeLarge}
|
||||||
|
showCaption={mediaAttachments.length === 1}
|
||||||
|
lang={language}
|
||||||
|
altIndex={
|
||||||
|
showMultipleMediaCaptions && !!media.description && i + 1
|
||||||
|
}
|
||||||
to={`/${instance}/s/${id}?${
|
to={`/${instance}/s/${id}?${
|
||||||
withinContext ? 'media' : 'media-only'
|
withinContext ? 'media' : 'media-only'
|
||||||
}=${i + 1}`}
|
}=${i + 1}`}
|
||||||
|
@ -1280,7 +1363,8 @@ function Status({
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
</MultipleMediaFigure>
|
||||||
)}
|
)}
|
||||||
{!!card &&
|
{!!card &&
|
||||||
card?.url !== status.url &&
|
card?.url !== status.url &&
|
||||||
|
@ -1448,7 +1532,7 @@ function Status({
|
||||||
statusID={showEdited}
|
statusID={showEdited}
|
||||||
instance={instance}
|
instance={instance}
|
||||||
fetchStatusHistory={() => {
|
fetchStatusHistory={() => {
|
||||||
return masto.v1.statuses.listHistory(showEdited);
|
return masto.v1.statuses.$select(showEdited).history.list();
|
||||||
}}
|
}}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
setShowEdited(false);
|
setShowEdited(false);
|
||||||
|
@ -1477,6 +1561,19 @@ function Status({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function MultipleMediaFigure(props) {
|
||||||
|
const { enabled, children, lang, captionChildren } = props;
|
||||||
|
if (!enabled || !captionChildren) return children;
|
||||||
|
return (
|
||||||
|
<figure class="media-figure-multiple">
|
||||||
|
{children}
|
||||||
|
<figcaption lang={lang} dir="auto">
|
||||||
|
{captionChildren}
|
||||||
|
</figcaption>
|
||||||
|
</figure>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function Card({ card, instance }) {
|
function Card({ card, instance }) {
|
||||||
const snapStates = useSnapshot(states);
|
const snapStates = useSnapshot(states);
|
||||||
const {
|
const {
|
||||||
|
@ -1485,14 +1582,18 @@ function Card({ card, instance }) {
|
||||||
description,
|
description,
|
||||||
html,
|
html,
|
||||||
providerName,
|
providerName,
|
||||||
|
providerUrl,
|
||||||
authorName,
|
authorName,
|
||||||
|
authorUrl,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
image,
|
image,
|
||||||
|
imageDescription,
|
||||||
url,
|
url,
|
||||||
type,
|
type,
|
||||||
embedUrl,
|
embedUrl,
|
||||||
language,
|
language,
|
||||||
|
publishedAt,
|
||||||
} = card;
|
} = card;
|
||||||
|
|
||||||
/* type
|
/* type
|
||||||
|
@ -1518,7 +1619,7 @@ function Card({ card, instance }) {
|
||||||
// NOTE: This is for quote post
|
// NOTE: This is for quote post
|
||||||
// (async () => {
|
// (async () => {
|
||||||
// const { masto } = api({ instance });
|
// const { masto } = api({ instance });
|
||||||
// const status = await masto.v1.statuses.fetch(id);
|
// const status = await masto.v1.statuses.$select(id).fetch();
|
||||||
// saveStatus(status, instance);
|
// saveStatus(status, instance);
|
||||||
// setCardStatusID(id);
|
// setCardStatusID(id);
|
||||||
// })();
|
// })();
|
||||||
|
@ -1565,7 +1666,7 @@ function Card({ card, instance }) {
|
||||||
width={width}
|
width={width}
|
||||||
height={height}
|
height={height}
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
alt=""
|
alt={imageDescription || ''}
|
||||||
onError={(e) => {
|
onError={(e) => {
|
||||||
try {
|
try {
|
||||||
e.target.style.display = 'none';
|
e.target.style.display = 'none';
|
||||||
|
@ -1738,15 +1839,16 @@ function ReactionsModal({ statusID, instance, onClose }) {
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
if (firstLoad) {
|
if (firstLoad) {
|
||||||
reblogIterator.current = masto.v1.statuses.listRebloggedBy(statusID, {
|
reblogIterator.current = masto.v1.statuses
|
||||||
limit: REACTIONS_LIMIT,
|
.$select(statusID)
|
||||||
});
|
.rebloggedBy.list({
|
||||||
favouriteIterator.current = masto.v1.statuses.listFavouritedBy(
|
|
||||||
statusID,
|
|
||||||
{
|
|
||||||
limit: REACTIONS_LIMIT,
|
limit: REACTIONS_LIMIT,
|
||||||
},
|
});
|
||||||
);
|
favouriteIterator.current = masto.v1.statuses
|
||||||
|
.$select(statusID)
|
||||||
|
.favouritedBy.list({
|
||||||
|
limit: REACTIONS_LIMIT,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
const [{ value: reblogResults }, { value: favouriteResults }] =
|
const [{ value: reblogResults }, { value: favouriteResults }] =
|
||||||
await Promise.allSettled([
|
await Promise.allSettled([
|
||||||
|
@ -1976,21 +2078,24 @@ function _unfurlMastodonLink(instance, url) {
|
||||||
if (statusMatch) {
|
if (statusMatch) {
|
||||||
const id = statusMatch[3];
|
const id = statusMatch[3];
|
||||||
const { masto } = api({ instance: domain });
|
const { masto } = api({ instance: domain });
|
||||||
remoteInstanceFetch = masto.v1.statuses.fetch(id).then((status) => {
|
remoteInstanceFetch = masto.v1.statuses
|
||||||
if (status?.id) {
|
.$select(id)
|
||||||
return {
|
.fetch()
|
||||||
status,
|
.then((status) => {
|
||||||
instance: domain,
|
if (status?.id) {
|
||||||
};
|
return {
|
||||||
} else {
|
status,
|
||||||
throw new Error('No results');
|
instance: domain,
|
||||||
}
|
};
|
||||||
});
|
} else {
|
||||||
|
throw new Error('No results');
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const { masto } = api({ instance });
|
const { masto } = api({ instance });
|
||||||
const mastoSearchFetch = masto.v2
|
const mastoSearchFetch = masto.v2.search
|
||||||
.search({
|
.fetch({
|
||||||
q: url,
|
q: url,
|
||||||
type: 'statuses',
|
type: 'statuses',
|
||||||
resolve: true,
|
resolve: true,
|
||||||
|
@ -2060,11 +2165,7 @@ function nicePostURL(url) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const unfurlMastodonLink = throttle(
|
const unfurlMastodonLink = throttle(_unfurlMastodonLink);
|
||||||
mem(_unfurlMastodonLink, {
|
|
||||||
cacheKey: (instance, url) => `${instance}:${url}`,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
function FilteredStatus({ status, filterInfo, instance, containerProps = {} }) {
|
function FilteredStatus({ status, filterInfo, instance, containerProps = {} }) {
|
||||||
const {
|
const {
|
||||||
|
@ -2087,7 +2188,7 @@ function FilteredStatus({ status, filterInfo, instance, containerProps = {} }) {
|
||||||
threshold: 600,
|
threshold: 600,
|
||||||
captureEvent: true,
|
captureEvent: true,
|
||||||
detect: 'touch',
|
detect: 'touch',
|
||||||
cancelOnMovement: 4, // true allows movement of up to 25 pixels
|
cancelOnMovement: 2, // true allows movement of up to 25 pixels
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -14,10 +14,15 @@ import useScroll from '../utils/useScroll';
|
||||||
|
|
||||||
import Icon from './icon';
|
import Icon from './icon';
|
||||||
import Link from './link';
|
import Link from './link';
|
||||||
import Loader from './loader';
|
|
||||||
import NavMenu from './nav-menu';
|
import NavMenu from './nav-menu';
|
||||||
import Status from './status';
|
import Status from './status';
|
||||||
|
|
||||||
|
const scrollIntoViewOptions = {
|
||||||
|
block: 'nearest',
|
||||||
|
inline: 'center',
|
||||||
|
behavior: 'smooth',
|
||||||
|
};
|
||||||
|
|
||||||
function Timeline({
|
function Timeline({
|
||||||
title,
|
title,
|
||||||
titleComponent,
|
titleComponent,
|
||||||
|
@ -112,7 +117,7 @@ function Timeline({
|
||||||
}
|
}
|
||||||
if (nextItem) {
|
if (nextItem) {
|
||||||
nextItem.focus();
|
nextItem.focus();
|
||||||
nextItem.scrollIntoViewIfNeeded?.();
|
nextItem.scrollIntoView(scrollIntoViewOptions);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// If active status is not in viewport, get the topmost status-link in viewport
|
// If active status is not in viewport, get the topmost status-link in viewport
|
||||||
|
@ -122,7 +127,7 @@ function Timeline({
|
||||||
});
|
});
|
||||||
if (topmostItem) {
|
if (topmostItem) {
|
||||||
topmostItem.focus();
|
topmostItem.focus();
|
||||||
topmostItem.scrollIntoViewIfNeeded?.();
|
topmostItem.scrollIntoView(scrollIntoViewOptions);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -151,7 +156,7 @@ function Timeline({
|
||||||
}
|
}
|
||||||
if (prevItem) {
|
if (prevItem) {
|
||||||
prevItem.focus();
|
prevItem.focus();
|
||||||
prevItem.scrollIntoViewIfNeeded?.();
|
prevItem.scrollIntoView(scrollIntoViewOptions);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// If active status is not in viewport, get the topmost status-link in viewport
|
// If active status is not in viewport, get the topmost status-link in viewport
|
||||||
|
@ -161,7 +166,7 @@ function Timeline({
|
||||||
});
|
});
|
||||||
if (topmostItem) {
|
if (topmostItem) {
|
||||||
topmostItem.focus();
|
topmostItem.focus();
|
||||||
topmostItem.scrollIntoViewIfNeeded?.();
|
topmostItem.scrollIntoView(scrollIntoViewOptions);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -413,7 +418,7 @@ function Timeline({
|
||||||
const isMiddle = i > 0 && i < items.length - 1;
|
const isMiddle = i > 0 && i < items.length - 1;
|
||||||
const isSpoiler = item.sensitive && !!item.spoilerText;
|
const isSpoiler = item.sensitive && !!item.spoilerText;
|
||||||
const showCompact =
|
const showCompact =
|
||||||
(isSpoiler && i > 0) ||
|
(!_differentAuthor && isSpoiler && i > 0) ||
|
||||||
(manyItems &&
|
(manyItems &&
|
||||||
isMiddle &&
|
isMiddle &&
|
||||||
(type === 'thread' ||
|
(type === 'thread' ||
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { useEffect, useRef, useState } from 'preact/hooks';
|
||||||
import sourceLanguages from '../data/lingva-source-languages';
|
import sourceLanguages from '../data/lingva-source-languages';
|
||||||
import getTranslateTargetLanguage from '../utils/get-translate-target-language';
|
import getTranslateTargetLanguage from '../utils/get-translate-target-language';
|
||||||
import localeCode2Text from '../utils/localeCode2Text';
|
import localeCode2Text from '../utils/localeCode2Text';
|
||||||
|
import pmem from '../utils/pmem';
|
||||||
|
|
||||||
import Icon from './icon';
|
import Icon from './icon';
|
||||||
import Loader from './loader';
|
import Loader from './loader';
|
||||||
|
@ -25,7 +26,7 @@ const LINGVA_INSTANCES = [
|
||||||
];
|
];
|
||||||
let currentLingvaInstance = 0;
|
let currentLingvaInstance = 0;
|
||||||
|
|
||||||
function lingvaTranslate(text, source, target) {
|
function _lingvaTranslate(text, source, target) {
|
||||||
console.log('TRANSLATE', text, source, target);
|
console.log('TRANSLATE', text, source, target);
|
||||||
const fetchCall = () => {
|
const fetchCall = () => {
|
||||||
let instance = LINGVA_INSTANCES[currentLingvaInstance];
|
let instance = LINGVA_INSTANCES[currentLingvaInstance];
|
||||||
|
@ -55,11 +56,18 @@ function lingvaTranslate(text, source, target) {
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
// return masto.v1.statuses.translate(id, {
|
// return masto.v1.statuses.$select(id).translate({
|
||||||
// lang: DEFAULT_LANG,
|
// lang: DEFAULT_LANG,
|
||||||
// });
|
// });
|
||||||
}
|
}
|
||||||
const throttledLingvaTranslate = throttle(lingvaTranslate);
|
const TRANSLATED_MAX_AGE = 1000 * 60 * 60; // 1 hour
|
||||||
|
const lingvaTranslate = pmem(_lingvaTranslate, {
|
||||||
|
maxAge: TRANSLATED_MAX_AGE,
|
||||||
|
});
|
||||||
|
const throttledLingvaTranslate = pmem(throttle(lingvaTranslate), {
|
||||||
|
// I know, this is double-layered memoization
|
||||||
|
maxAge: TRANSLATED_MAX_AGE,
|
||||||
|
});
|
||||||
|
|
||||||
function TranslationBlock({
|
function TranslationBlock({
|
||||||
forceTranslate,
|
forceTranslate,
|
||||||
|
|
|
@ -47,7 +47,7 @@
|
||||||
--reply-to-color: var(--orange-color);
|
--reply-to-color: var(--orange-color);
|
||||||
--reply-to-text-color: #b36200;
|
--reply-to-text-color: #b36200;
|
||||||
--favourite-color: var(--red-color);
|
--favourite-color: var(--red-color);
|
||||||
--reply-to-faded-color: #ffa60030;
|
--reply-to-faded-color: #ffa60020;
|
||||||
--outline-color: rgba(128, 128, 128, 0.2);
|
--outline-color: rgba(128, 128, 128, 0.2);
|
||||||
--outline-hover-color: rgba(128, 128, 128, 0.7);
|
--outline-hover-color: rgba(128, 128, 128, 0.7);
|
||||||
--divider-color: rgba(0, 0, 0, 0.1);
|
--divider-color: rgba(0, 0, 0, 0.1);
|
||||||
|
@ -64,9 +64,9 @@
|
||||||
--close-button-hover-color: rgba(0, 0, 0, 1);
|
--close-button-hover-color: rgba(0, 0, 0, 1);
|
||||||
|
|
||||||
/* Video colors won't change based on color scheme */
|
/* Video colors won't change based on color scheme */
|
||||||
--video-fg-color: #f0f2f5;
|
--media-fg-color: #f0f2f5;
|
||||||
--video-bg-color: #242526;
|
--media-bg-color: #242526;
|
||||||
--video-outline-color: color-mix(in lch, var(--video-fg-color), transparent);
|
--media-outline-color: color-mix(in lch, var(--media-fg-color), transparent);
|
||||||
|
|
||||||
--timing-function: cubic-bezier(0.3, 0.5, 0, 1);
|
--timing-function: cubic-bezier(0.3, 0.5, 0, 1);
|
||||||
}
|
}
|
||||||
|
@ -92,9 +92,14 @@
|
||||||
--link-light-color: #6494ed99;
|
--link-light-color: #6494ed99;
|
||||||
--link-faded-color: #6494ed88;
|
--link-faded-color: #6494ed88;
|
||||||
--link-bg-hover-color: #34353799;
|
--link-bg-hover-color: #34353799;
|
||||||
|
--link-visited-color: color-mix(
|
||||||
|
in lch,
|
||||||
|
mediumslateblue 70%,
|
||||||
|
var(--text-color) 30%
|
||||||
|
);
|
||||||
--reblog-faded-color: #b190f141;
|
--reblog-faded-color: #b190f141;
|
||||||
--reply-to-text-color: var(--reply-to-color);
|
--reply-to-text-color: var(--reply-to-color);
|
||||||
--reply-to-faded-color: #ffa60027;
|
--reply-to-faded-color: #ffa60017;
|
||||||
--divider-color: rgba(255, 255, 255, 0.1);
|
--divider-color: rgba(255, 255, 255, 0.1);
|
||||||
--bg-blur-color: #24252699;
|
--bg-blur-color: #24252699;
|
||||||
--backdrop-color: rgba(0, 0, 0, 0.5);
|
--backdrop-color: rgba(0, 0, 0, 0.5);
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Menu, MenuItem } from '@szhsin/react-menu';
|
import { MenuItem } from '@szhsin/react-menu';
|
||||||
import { useEffect, useMemo, useRef, useState } from 'preact/hooks';
|
import { useEffect, useMemo, useRef, useState } from 'preact/hooks';
|
||||||
import { useParams, useSearchParams } from 'react-router-dom';
|
import { useParams, useSearchParams } from 'react-router-dom';
|
||||||
import { useSnapshot } from 'valtio';
|
import { useSnapshot } from 'valtio';
|
||||||
|
@ -31,7 +31,8 @@ function AccountStatuses() {
|
||||||
const results = [];
|
const results = [];
|
||||||
if (firstLoad) {
|
if (firstLoad) {
|
||||||
const { value: pinnedStatuses } = await masto.v1.accounts
|
const { value: pinnedStatuses } = await masto.v1.accounts
|
||||||
.listStatuses(id, {
|
.$select(id)
|
||||||
|
.statuses.list({
|
||||||
pinned: true,
|
pinned: true,
|
||||||
})
|
})
|
||||||
.next();
|
.next();
|
||||||
|
@ -53,13 +54,15 @@ function AccountStatuses() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (firstLoad || !accountStatusesIterator.current) {
|
if (firstLoad || !accountStatusesIterator.current) {
|
||||||
accountStatusesIterator.current = masto.v1.accounts.listStatuses(id, {
|
accountStatusesIterator.current = masto.v1.accounts
|
||||||
limit: LIMIT,
|
.$select(id)
|
||||||
exclude_replies: excludeReplies,
|
.statuses.list({
|
||||||
exclude_reblogs: excludeBoosts,
|
limit: LIMIT,
|
||||||
only_media: media,
|
exclude_replies: excludeReplies,
|
||||||
tagged,
|
exclude_reblogs: excludeBoosts,
|
||||||
});
|
only_media: media,
|
||||||
|
tagged,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
const { value, done } = await accountStatusesIterator.current.next();
|
const { value, done } = await accountStatusesIterator.current.next();
|
||||||
if (value?.length) {
|
if (value?.length) {
|
||||||
|
@ -86,14 +89,16 @@ function AccountStatuses() {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
const acc = await masto.v1.accounts.fetch(id);
|
const acc = await masto.v1.accounts.$select(id).fetch();
|
||||||
console.log(acc);
|
console.log(acc);
|
||||||
setAccount(acc);
|
setAccount(acc);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const featuredTags = await masto.v1.accounts.listFeaturedTags(id);
|
const featuredTags = await masto.v1.accounts
|
||||||
|
.$select(id)
|
||||||
|
.featuredTags.list(id);
|
||||||
console.log({ featuredTags });
|
console.log({ featuredTags });
|
||||||
setFeaturedTags(featuredTags);
|
setFeaturedTags(featuredTags);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -113,7 +118,7 @@ function AccountStatuses() {
|
||||||
<AccountInfo
|
<AccountInfo
|
||||||
instance={instance}
|
instance={instance}
|
||||||
account={cachedAccount || id}
|
account={cachedAccount || id}
|
||||||
fetchAccount={() => masto.v1.accounts.fetch(id)}
|
fetchAccount={() => masto.v1.accounts.$select(id).fetch()}
|
||||||
authenticated={authenticated}
|
authenticated={authenticated}
|
||||||
standalone
|
standalone
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -52,9 +52,9 @@ function Accounts({ onClose }) {
|
||||||
onDblClick={async () => {
|
onDblClick={async () => {
|
||||||
if (isCurrent) {
|
if (isCurrent) {
|
||||||
try {
|
try {
|
||||||
const info = await masto.v1.accounts.fetch(
|
const info = await masto.v1.accounts
|
||||||
account.info.id,
|
.$select(account.info.id)
|
||||||
);
|
.fetch();
|
||||||
console.log('fetched account info', info);
|
console.log('fetched account info', info);
|
||||||
account.info = info;
|
account.info = info;
|
||||||
store.local.setJSON('accounts', accounts);
|
store.local.setJSON('accounts', accounts);
|
||||||
|
|
|
@ -13,7 +13,7 @@ const LIMIT = 20;
|
||||||
|
|
||||||
function Following({ title, path, id, ...props }) {
|
function Following({ title, path, id, ...props }) {
|
||||||
useTitle(title || 'Following', path || '/following');
|
useTitle(title || 'Following', path || '/following');
|
||||||
const { masto, instance } = api();
|
const { masto, streaming, instance } = api();
|
||||||
const snapStates = useSnapshot(states);
|
const snapStates = useSnapshot(states);
|
||||||
const homeIterator = useRef();
|
const homeIterator = useRef();
|
||||||
const latestItem = useRef();
|
const latestItem = useRef();
|
||||||
|
@ -22,7 +22,7 @@ function Following({ title, path, id, ...props }) {
|
||||||
|
|
||||||
async function fetchHome(firstLoad) {
|
async function fetchHome(firstLoad) {
|
||||||
if (firstLoad || !homeIterator.current) {
|
if (firstLoad || !homeIterator.current) {
|
||||||
homeIterator.current = masto.v1.timelines.listHome({ limit: LIMIT });
|
homeIterator.current = masto.v1.timelines.home.list({ limit: LIMIT });
|
||||||
}
|
}
|
||||||
const results = await homeIterator.current.next();
|
const results = await homeIterator.current.next();
|
||||||
let { value } = results;
|
let { value } = results;
|
||||||
|
@ -53,8 +53,8 @@ function Following({ title, path, id, ...props }) {
|
||||||
|
|
||||||
async function checkForUpdates() {
|
async function checkForUpdates() {
|
||||||
try {
|
try {
|
||||||
const results = await masto.v1.timelines
|
const results = await masto.v1.timelines.home
|
||||||
.listHome({
|
.list({
|
||||||
limit: 5,
|
limit: 5,
|
||||||
since_id: latestItem.current,
|
since_id: latestItem.current,
|
||||||
})
|
})
|
||||||
|
@ -75,52 +75,33 @@ function Following({ title, path, id, ...props }) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const ws = useRef();
|
|
||||||
const streamUser = async () => {
|
|
||||||
console.log('🎏 Start streaming user', ws.current);
|
|
||||||
if (
|
|
||||||
ws.current &&
|
|
||||||
(ws.current.readyState === WebSocket.CONNECTING ||
|
|
||||||
ws.current.readyState === WebSocket.OPEN)
|
|
||||||
) {
|
|
||||||
console.log('🎏 Streaming user already open');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const stream = await masto.v1.stream.streamUser();
|
|
||||||
ws.current = stream.ws;
|
|
||||||
ws.current.__id = Math.random();
|
|
||||||
console.log('🎏 Streaming user', ws.current);
|
|
||||||
|
|
||||||
stream.on('status.update', (status) => {
|
|
||||||
console.log(`🔄 Status ${status.id} updated`);
|
|
||||||
saveStatus(status, instance);
|
|
||||||
});
|
|
||||||
|
|
||||||
stream.on('delete', (statusID) => {
|
|
||||||
console.log(`❌ Status ${statusID} deleted`);
|
|
||||||
// delete states.statuses[statusID];
|
|
||||||
const s = getStatus(statusID, instance);
|
|
||||||
if (s) s._deleted = true;
|
|
||||||
});
|
|
||||||
|
|
||||||
stream.ws.onclose = () => {
|
|
||||||
console.log('🎏 Streaming user closed');
|
|
||||||
};
|
|
||||||
|
|
||||||
return stream;
|
|
||||||
};
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let stream;
|
let sub;
|
||||||
(async () => {
|
(async () => {
|
||||||
stream = await streamUser();
|
if (streaming) {
|
||||||
|
sub = streaming.user.subscribe();
|
||||||
|
console.log('🎏 Streaming user', sub);
|
||||||
|
for await (const entry of sub) {
|
||||||
|
if (!sub) break;
|
||||||
|
if (entry.event === 'status.update') {
|
||||||
|
const status = entry.payload;
|
||||||
|
console.log(`🔄 Status ${status.id} updated`);
|
||||||
|
saveStatus(status, instance);
|
||||||
|
} else if (entry.event === 'delete') {
|
||||||
|
const statusID = entry.payload;
|
||||||
|
console.log(`❌ Status ${statusID} deleted`);
|
||||||
|
// delete states.statuses[statusID];
|
||||||
|
const s = getStatus(statusID, instance);
|
||||||
|
if (s) s._deleted = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
})();
|
})();
|
||||||
return () => {
|
return () => {
|
||||||
if (stream) {
|
sub?.unsubscribe?.();
|
||||||
stream.ws.close();
|
sub = null;
|
||||||
ws.current = null;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}, []);
|
}, [streaming]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Timeline
|
<Timeline
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import {
|
import {
|
||||||
FocusableItem,
|
FocusableItem,
|
||||||
Menu,
|
|
||||||
MenuDivider,
|
MenuDivider,
|
||||||
MenuGroup,
|
MenuGroup,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
|
@ -47,7 +46,7 @@ function Hashtags({ columnMode, ...props }) {
|
||||||
const maxID = useRef(undefined);
|
const maxID = useRef(undefined);
|
||||||
async function fetchHashtags(firstLoad) {
|
async function fetchHashtags(firstLoad) {
|
||||||
// if (firstLoad || !hashtagsIterator.current) {
|
// if (firstLoad || !hashtagsIterator.current) {
|
||||||
// hashtagsIterator.current = masto.v1.timelines.listHashtag(hashtag, {
|
// hashtagsIterator.current = masto.v1.timelines.tag.$select(hashtag).list({
|
||||||
// limit: LIMIT,
|
// limit: LIMIT,
|
||||||
// any: hashtags.slice(1),
|
// any: hashtags.slice(1),
|
||||||
// });
|
// });
|
||||||
|
@ -55,8 +54,9 @@ function Hashtags({ columnMode, ...props }) {
|
||||||
// const results = await hashtagsIterator.current.next();
|
// const results = await hashtagsIterator.current.next();
|
||||||
|
|
||||||
// NOTE: Temporary fix for listHashtag not persisting `any` in subsequent calls.
|
// NOTE: Temporary fix for listHashtag not persisting `any` in subsequent calls.
|
||||||
const results = await masto.v1.timelines
|
const results = await masto.v1.timelines.tag
|
||||||
.listHashtag(hashtag, {
|
.$select(hashtag)
|
||||||
|
.list({
|
||||||
limit: LIMIT,
|
limit: LIMIT,
|
||||||
any: hashtags.slice(1),
|
any: hashtags.slice(1),
|
||||||
maxId: firstLoad ? undefined : maxID.current,
|
maxId: firstLoad ? undefined : maxID.current,
|
||||||
|
@ -82,8 +82,9 @@ function Hashtags({ columnMode, ...props }) {
|
||||||
|
|
||||||
async function checkForUpdates() {
|
async function checkForUpdates() {
|
||||||
try {
|
try {
|
||||||
const results = await masto.v1.timelines
|
const results = await masto.v1.timelines.tag
|
||||||
.listHashtag(hashtag, {
|
.$select(hashtag)
|
||||||
|
.list({
|
||||||
limit: 1,
|
limit: 1,
|
||||||
any: hashtags.slice(1),
|
any: hashtags.slice(1),
|
||||||
since_id: latestItem.current,
|
since_id: latestItem.current,
|
||||||
|
@ -105,7 +106,7 @@ function Hashtags({ columnMode, ...props }) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
const info = await masto.v1.tags.fetch(hashtag);
|
const info = await masto.v1.tags.$select(hashtag).fetch();
|
||||||
console.log(info);
|
console.log(info);
|
||||||
setInfo(info);
|
setInfo(info);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -164,7 +165,8 @@ function Hashtags({ columnMode, ...props }) {
|
||||||
// return;
|
// return;
|
||||||
// }
|
// }
|
||||||
masto.v1.tags
|
masto.v1.tags
|
||||||
.unfollow(hashtag)
|
.$select(hashtag)
|
||||||
|
.unfollow()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
setInfo({ ...info, following: false });
|
setInfo({ ...info, following: false });
|
||||||
showToast(`Unfollowed #${hashtag}`);
|
showToast(`Unfollowed #${hashtag}`);
|
||||||
|
@ -178,7 +180,8 @@ function Hashtags({ columnMode, ...props }) {
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
masto.v1.tags
|
masto.v1.tags
|
||||||
.follow(hashtag)
|
.$select(hashtag)
|
||||||
|
.follow()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
setInfo({ ...info, following: true });
|
setInfo({ ...info, following: true });
|
||||||
showToast(`Followed #${hashtag}`);
|
showToast(`Followed #${hashtag}`);
|
||||||
|
@ -258,11 +261,14 @@ function Hashtags({ columnMode, ...props }) {
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
hashtags.splice(i, 1);
|
hashtags.splice(i, 1);
|
||||||
hashtags.sort();
|
hashtags.sort();
|
||||||
navigate(
|
// navigate(
|
||||||
instance
|
// instance
|
||||||
? `/${instance}/t/${hashtags.join('+')}`
|
// ? `/${instance}/t/${hashtags.join('+')}`
|
||||||
: `/t/${hashtags.join('+')}`,
|
// : `/t/${hashtags.join('+')}`,
|
||||||
);
|
// );
|
||||||
|
location.hash = instance
|
||||||
|
? `/${instance}/t/${hashtags.join('+')}`
|
||||||
|
: `/t/${hashtags.join('+')}`;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon icon="x" alt="Remove hashtag" class="danger-icon" />
|
<Icon icon="x" alt="Remove hashtag" class="danger-icon" />
|
||||||
|
@ -317,7 +323,8 @@ function Hashtags({ columnMode, ...props }) {
|
||||||
}
|
}
|
||||||
if (newInstance) {
|
if (newInstance) {
|
||||||
newInstance = newInstance.toLowerCase().trim();
|
newInstance = newInstance.toLowerCase().trim();
|
||||||
navigate(`/${newInstance}/t/${hashtags.join('+')}`);
|
// navigate(`/${newInstance}/t/${hashtags.join('+')}`);
|
||||||
|
location.hash = `/${newInstance}/t/${hashtags.join('+')}`;
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
|
@ -13,7 +13,6 @@ import Notification from '../components/notification';
|
||||||
import { api } from '../utils/api';
|
import { api } from '../utils/api';
|
||||||
import db from '../utils/db';
|
import db from '../utils/db';
|
||||||
import groupNotifications from '../utils/group-notifications';
|
import groupNotifications from '../utils/group-notifications';
|
||||||
import openCompose from '../utils/open-compose';
|
|
||||||
import states, { saveStatus } from '../utils/states';
|
import states, { saveStatus } from '../utils/states';
|
||||||
import { getCurrentAccountNS } from '../utils/store-utils';
|
import { getCurrentAccountNS } from '../utils/store-utils';
|
||||||
|
|
||||||
|
@ -49,24 +48,6 @@ function Home() {
|
||||||
headerEnd={<NotificationsLink />}
|
headerEnd={<NotificationsLink />}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{/* <button
|
|
||||||
// hidden={scrollDirection === 'end' && !nearReachStart}
|
|
||||||
type="button"
|
|
||||||
id="compose-button"
|
|
||||||
onClick={(e) => {
|
|
||||||
if (e.shiftKey) {
|
|
||||||
const newWin = openCompose();
|
|
||||||
if (!newWin) {
|
|
||||||
alert('Looks like your browser is blocking popups.');
|
|
||||||
states.showCompose = true;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
states.showCompose = true;
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Icon icon="quill" size="xl" alt="Compose" />
|
|
||||||
</button> */}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,7 +32,7 @@ function List(props) {
|
||||||
const listIterator = useRef();
|
const listIterator = useRef();
|
||||||
async function fetchList(firstLoad) {
|
async function fetchList(firstLoad) {
|
||||||
if (firstLoad || !listIterator.current) {
|
if (firstLoad || !listIterator.current) {
|
||||||
listIterator.current = masto.v1.timelines.listList(id, {
|
listIterator.current = masto.v1.timelines.list.$select(id).list({
|
||||||
limit: LIMIT,
|
limit: LIMIT,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -56,7 +56,7 @@ function List(props) {
|
||||||
|
|
||||||
async function checkForUpdates() {
|
async function checkForUpdates() {
|
||||||
try {
|
try {
|
||||||
const results = await masto.v1.timelines.listList(id, {
|
const results = await masto.v1.timelines.list.$select(id).list({
|
||||||
limit: 1,
|
limit: 1,
|
||||||
since_id: latestItem.current,
|
since_id: latestItem.current,
|
||||||
});
|
});
|
||||||
|
@ -77,7 +77,7 @@ function List(props) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
const list = await masto.v1.lists.fetch(id);
|
const list = await masto.v1.lists.$select(id).fetch();
|
||||||
setList(list);
|
setList(list);
|
||||||
// setTitle(list.title);
|
// setTitle(list.title);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -200,9 +200,11 @@ function ListManageMembers({ listID, onClose }) {
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
if (firstLoad || !membersIterator.current) {
|
if (firstLoad || !membersIterator.current) {
|
||||||
membersIterator.current = masto.v1.lists.listAccounts(listID, {
|
membersIterator.current = masto.v1.lists
|
||||||
limit: MEMBERS_LIMIT,
|
.$select(listID)
|
||||||
});
|
.accounts.list({
|
||||||
|
limit: MEMBERS_LIMIT,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
const results = await membersIterator.current.next();
|
const results = await membersIterator.current.next();
|
||||||
let { done, value } = results;
|
let { done, value } = results;
|
||||||
|
@ -274,7 +276,7 @@ function RemoveAddButton({ account, listID }) {
|
||||||
setUIState('loading');
|
setUIState('loading');
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
await masto.v1.lists.addAccount(listID, {
|
await masto.v1.lists.$select(listID).accounts.create({
|
||||||
accountIds: [account.id],
|
accountIds: [account.id],
|
||||||
});
|
});
|
||||||
setUIState('default');
|
setUIState('default');
|
||||||
|
@ -290,7 +292,7 @@ function RemoveAddButton({ account, listID }) {
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
await masto.v1.lists.removeAccount(listID, {
|
await masto.v1.lists.$select(listID).accounts.remove({
|
||||||
accountIds: [account.id],
|
accountIds: [account.id],
|
||||||
});
|
});
|
||||||
setUIState('default');
|
setUIState('default');
|
||||||
|
|
|
@ -92,11 +92,16 @@ function Notifications({ columnMode }) {
|
||||||
return allNotifications;
|
return allNotifications;
|
||||||
}
|
}
|
||||||
|
|
||||||
function fetchFollowRequests() {
|
async function fetchFollowRequests() {
|
||||||
// Note: no pagination here yet because this better be on a separate page. Should be rare use-case???
|
// Note: no pagination here yet because this better be on a separate page. Should be rare use-case???
|
||||||
return masto.v1.followRequests.list({
|
try {
|
||||||
limit: 80,
|
return await masto.v1.followRequests.list({
|
||||||
});
|
limit: 80,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
// Silently fail
|
||||||
|
return [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadFollowRequests = () => {
|
const loadFollowRequests = () => {
|
||||||
|
@ -112,8 +117,13 @@ function Notifications({ columnMode }) {
|
||||||
})();
|
})();
|
||||||
};
|
};
|
||||||
|
|
||||||
function fetchAnnouncements() {
|
async function fetchAnnouncements() {
|
||||||
return masto.v1.announcements.list();
|
try {
|
||||||
|
return await masto.v1.announcements.list();
|
||||||
|
} catch (e) {
|
||||||
|
// Silently fail
|
||||||
|
return [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadNotifications = (firstLoad) => {
|
const loadNotifications = (firstLoad) => {
|
||||||
|
@ -379,39 +389,43 @@ function Notifications({ columnMode }) {
|
||||||
)}
|
)}
|
||||||
{snapStates.notifications.length ? (
|
{snapStates.notifications.length ? (
|
||||||
<>
|
<>
|
||||||
{snapStates.notifications.map((notification) => {
|
{snapStates.notifications
|
||||||
if (onlyMentions && notification.type !== 'mention') {
|
// This is leaked from Notifications popover
|
||||||
return null;
|
.filter((n) => n.type !== 'follow_request')
|
||||||
}
|
.map((notification) => {
|
||||||
const notificationDay = new Date(notification.createdAt);
|
if (onlyMentions && notification.type !== 'mention') {
|
||||||
const differentDay =
|
return null;
|
||||||
notificationDay.toDateString() !== currentDay.toDateString();
|
}
|
||||||
if (differentDay) {
|
const notificationDay = new Date(notification.createdAt);
|
||||||
currentDay = notificationDay;
|
const differentDay =
|
||||||
}
|
notificationDay.toDateString() !== currentDay.toDateString();
|
||||||
// if notificationDay is yesterday, show "Yesterday"
|
if (differentDay) {
|
||||||
// if notificationDay is before yesterday, show date
|
currentDay = notificationDay;
|
||||||
const heading =
|
}
|
||||||
notificationDay.toDateString() === yesterdayDate.toDateString()
|
// if notificationDay is yesterday, show "Yesterday"
|
||||||
? 'Yesterday'
|
// if notificationDay is before yesterday, show date
|
||||||
: niceDateTime(currentDay, {
|
const heading =
|
||||||
hideTime: true,
|
notificationDay.toDateString() ===
|
||||||
});
|
yesterdayDate.toDateString()
|
||||||
return (
|
? 'Yesterday'
|
||||||
<>
|
: niceDateTime(currentDay, {
|
||||||
{differentDay && <h2 class="timeline-header">{heading}</h2>}
|
hideTime: true,
|
||||||
<Notification
|
});
|
||||||
instance={instance}
|
return (
|
||||||
notification={notification}
|
<>
|
||||||
key={notification.id}
|
{differentDay && <h2 class="timeline-header">{heading}</h2>}
|
||||||
reload={() => {
|
<Notification
|
||||||
loadNotifications(true);
|
instance={instance}
|
||||||
loadFollowRequests();
|
notification={notification}
|
||||||
}}
|
key={notification.id}
|
||||||
/>
|
reload={() => {
|
||||||
</>
|
loadNotifications(true);
|
||||||
);
|
loadFollowRequests();
|
||||||
})}
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
|
|
@ -29,7 +29,7 @@ function Public({ local, columnMode, ...props }) {
|
||||||
const publicIterator = useRef();
|
const publicIterator = useRef();
|
||||||
async function fetchPublic(firstLoad) {
|
async function fetchPublic(firstLoad) {
|
||||||
if (firstLoad || !publicIterator.current) {
|
if (firstLoad || !publicIterator.current) {
|
||||||
publicIterator.current = masto.v1.timelines.listPublic({
|
publicIterator.current = masto.v1.timelines.public.list({
|
||||||
limit: LIMIT,
|
limit: LIMIT,
|
||||||
local: isLocal,
|
local: isLocal,
|
||||||
});
|
});
|
||||||
|
@ -54,8 +54,8 @@ function Public({ local, columnMode, ...props }) {
|
||||||
|
|
||||||
async function checkForUpdates() {
|
async function checkForUpdates() {
|
||||||
try {
|
try {
|
||||||
const results = await masto.v1.timelines
|
const results = await masto.v1.timelines.public
|
||||||
.listPublic({
|
.list({
|
||||||
limit: 1,
|
limit: 1,
|
||||||
local: isLocal,
|
local: isLocal,
|
||||||
since_id: latestItem.current,
|
since_id: latestItem.current,
|
||||||
|
|
|
@ -90,7 +90,7 @@ function Search(props) {
|
||||||
if (authenticated) params.offset = offsetRef.current;
|
if (authenticated) params.offset = offsetRef.current;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const results = await masto.v2.search(params);
|
const results = await masto.v2.search.fetch(params);
|
||||||
console.log(results);
|
console.log(results);
|
||||||
if (type) {
|
if (type) {
|
||||||
if (firstLoad) {
|
if (firstLoad) {
|
||||||
|
|
|
@ -135,3 +135,8 @@
|
||||||
padding-inline: 16px;
|
padding-inline: 16px;
|
||||||
color: var(--text-insignificant-color);
|
color: var(--text-insignificant-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#settings-container .synced-icon {
|
||||||
|
color: var(--link-color);
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { useEffect, useRef, useState } from 'preact/hooks';
|
||||||
import { useSnapshot } from 'valtio';
|
import { useSnapshot } from 'valtio';
|
||||||
|
|
||||||
import logo from '../assets/logo.svg';
|
import logo from '../assets/logo.svg';
|
||||||
|
|
||||||
import Icon from '../components/icon';
|
import Icon from '../components/icon';
|
||||||
import Link from '../components/link';
|
import Link from '../components/link';
|
||||||
import RelativeTime from '../components/relative-time';
|
import RelativeTime from '../components/relative-time';
|
||||||
|
@ -34,7 +35,7 @@ function Settings({ onClose }) {
|
||||||
const currentTextSize = store.local.get('textSize') || DEFAULT_TEXT_SIZE;
|
const currentTextSize = store.local.get('textSize') || DEFAULT_TEXT_SIZE;
|
||||||
|
|
||||||
const [prefs, setPrefs] = useState(store.account.get('preferences') || {});
|
const [prefs, setPrefs] = useState(store.account.get('preferences') || {});
|
||||||
const { masto, authenticated } = api();
|
const { masto, authenticated, instance } = api();
|
||||||
// Get preferences every time Settings is opened
|
// Get preferences every time Settings is opened
|
||||||
// NOTE: Disabled for now because I don't expect this to change often. Also for some reason, the /api/v1/preferences endpoint is cached for a while and return old prefs if refresh immediately after changing them.
|
// NOTE: Disabled for now because I don't expect this to change often. Also for some reason, the /api/v1/preferences endpoint is cached for a while and return old prefs if refresh immediately after changing them.
|
||||||
// useEffect(() => {
|
// useEffect(() => {
|
||||||
|
@ -178,7 +179,8 @@ function Settings({ onClose }) {
|
||||||
<li>
|
<li>
|
||||||
<div>
|
<div>
|
||||||
<label for="posting-privacy-field">
|
<label for="posting-privacy-field">
|
||||||
Default visibility
|
Default visibility{' '}
|
||||||
|
<Icon icon="cloud" alt="Synced" class="synced-icon" />
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
@ -217,6 +219,19 @@ function Settings({ onClose }) {
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
|
<p class="section-postnote">
|
||||||
|
<Icon icon="cloud" alt="Synced" class="synced-icon" />{' '}
|
||||||
|
<small>
|
||||||
|
Synced to your instance server's settings.{' '}
|
||||||
|
<a
|
||||||
|
href={`https://${instance}/`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
Go to your instance ({instance}) for more settings.
|
||||||
|
</a>
|
||||||
|
</small>
|
||||||
|
</p>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<h3>Experiments</h3>
|
<h3>Experiments</h3>
|
||||||
|
@ -339,6 +354,7 @@ function Settings({ onClose }) {
|
||||||
<a
|
<a
|
||||||
href="https://github.com/thedaviddelta/lingva-translate"
|
href="https://github.com/thedaviddelta/lingva-translate"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
>
|
>
|
||||||
Lingva Translate
|
Lingva Translate
|
||||||
</a>
|
</a>
|
||||||
|
@ -435,6 +451,7 @@ function Settings({ onClose }) {
|
||||||
<a
|
<a
|
||||||
href="https://hachyderm.io/@phanpy"
|
href="https://hachyderm.io/@phanpy"
|
||||||
// target="_blank"
|
// target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
states.showAccount = 'phanpy@hachyderm.io';
|
states.showAccount = 'phanpy@hachyderm.io';
|
||||||
|
@ -458,6 +475,7 @@ function Settings({ onClose }) {
|
||||||
<a
|
<a
|
||||||
href="https://mastodon.social/@cheeaun"
|
href="https://mastodon.social/@cheeaun"
|
||||||
// target="_blank"
|
// target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
states.showAccount = 'cheeaun@mastodon.social';
|
states.showAccount = 'cheeaun@mastodon.social';
|
||||||
|
@ -468,9 +486,26 @@ function Settings({ onClose }) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p>
|
<p>
|
||||||
|
<a
|
||||||
|
href="https://github.com/sponsors/cheeaun"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
Sponsor
|
||||||
|
</a>{' '}
|
||||||
|
·{' '}
|
||||||
|
<a
|
||||||
|
href="https://www.buymeacoffee.com/cheeaun"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
Donate
|
||||||
|
</a>{' '}
|
||||||
|
·{' '}
|
||||||
<a
|
<a
|
||||||
href="https://github.com/cheeaun/phanpy/blob/main/PRIVACY.MD"
|
href="https://github.com/cheeaun/phanpy/blob/main/PRIVACY.MD"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
>
|
>
|
||||||
Privacy Policy
|
Privacy Policy
|
||||||
</a>
|
</a>
|
||||||
|
|
|
@ -14,7 +14,7 @@ import {
|
||||||
} from 'preact/hooks';
|
} from 'preact/hooks';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
import { InView } from 'react-intersection-observer';
|
import { InView } from 'react-intersection-observer';
|
||||||
import { matchPath, useParams, useSearchParams } from 'react-router-dom';
|
import { matchPath, useSearchParams } from 'react-router-dom';
|
||||||
import { useDebouncedCallback } from 'use-debounce';
|
import { useDebouncedCallback } from 'use-debounce';
|
||||||
import { useSnapshot } from 'valtio';
|
import { useSnapshot } from 'valtio';
|
||||||
|
|
||||||
|
@ -54,6 +54,12 @@ function resetScrollPosition(id) {
|
||||||
delete scrollPositions[id];
|
delete scrollPositions[id];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const scrollIntoViewOptions = {
|
||||||
|
block: 'nearest',
|
||||||
|
inline: 'center',
|
||||||
|
behavior: 'smooth',
|
||||||
|
};
|
||||||
|
|
||||||
function StatusPage(params) {
|
function StatusPage(params) {
|
||||||
const { id } = params;
|
const { id } = params;
|
||||||
const { masto, instance } = api({ instance: params.instance });
|
const { masto, instance } = api({ instance: params.instance });
|
||||||
|
@ -94,7 +100,7 @@ function StatusPage(params) {
|
||||||
if (!heroStatus && showMedia) {
|
if (!heroStatus && showMedia) {
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
const status = await masto.v1.statuses.fetch(id);
|
const status = await masto.v1.statuses.$select(id).fetch();
|
||||||
saveStatus(status, instance);
|
saveStatus(status, instance);
|
||||||
setHeroStatus(status);
|
setHeroStatus(status);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
@ -135,6 +141,7 @@ function StatusPage(params) {
|
||||||
mediaAttachments={mediaAttachments}
|
mediaAttachments={mediaAttachments}
|
||||||
statusID={mediaStatusID || id}
|
statusID={mediaStatusID || id}
|
||||||
instance={instance}
|
instance={instance}
|
||||||
|
lang={heroStatus?.language}
|
||||||
index={mediaIndex - 1}
|
index={mediaIndex - 1}
|
||||||
onClose={handleMediaClose}
|
onClose={handleMediaClose}
|
||||||
/>
|
/>
|
||||||
|
@ -228,12 +235,15 @@ function StatusThread({ id, closeLink = '/', instance: propInstance }) {
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
const heroFetch = () =>
|
const heroFetch = () =>
|
||||||
pRetry(() => masto.v1.statuses.fetch(id), {
|
pRetry(() => masto.v1.statuses.$select(id).fetch(), {
|
||||||
retries: 4,
|
retries: 4,
|
||||||
});
|
});
|
||||||
const contextFetch = pRetry(() => masto.v1.statuses.fetchContext(id), {
|
const contextFetch = pRetry(
|
||||||
retries: 8,
|
() => masto.v1.statuses.$select(id).context.fetch(),
|
||||||
});
|
{
|
||||||
|
retries: 8,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const hasStatus = !!snapStates.statuses[sKey];
|
const hasStatus = !!snapStates.statuses[sKey];
|
||||||
let heroStatus = snapStates.statuses[sKey];
|
let heroStatus = snapStates.statuses[sKey];
|
||||||
|
@ -554,7 +564,7 @@ function StatusThread({ id, closeLink = '/', instance: propInstance }) {
|
||||||
let nextStatus = allStatusLinks[activeStatusIndex + 1];
|
let nextStatus = allStatusLinks[activeStatusIndex + 1];
|
||||||
if (nextStatus) {
|
if (nextStatus) {
|
||||||
nextStatus.focus();
|
nextStatus.focus();
|
||||||
nextStatus.scrollIntoViewIfNeeded?.();
|
nextStatus.scrollIntoView(scrollIntoViewOptions);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// If active status is not in viewport, get the topmost status-link in viewport
|
// If active status is not in viewport, get the topmost status-link in viewport
|
||||||
|
@ -564,7 +574,7 @@ function StatusThread({ id, closeLink = '/', instance: propInstance }) {
|
||||||
});
|
});
|
||||||
if (topmostStatusLink) {
|
if (topmostStatusLink) {
|
||||||
topmostStatusLink.focus();
|
topmostStatusLink.focus();
|
||||||
topmostStatusLink.scrollIntoViewIfNeeded?.();
|
topmostStatusLink.scrollIntoView(scrollIntoViewOptions);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -588,7 +598,7 @@ function StatusThread({ id, closeLink = '/', instance: propInstance }) {
|
||||||
let prevStatus = allStatusLinks[activeStatusIndex - 1];
|
let prevStatus = allStatusLinks[activeStatusIndex - 1];
|
||||||
if (prevStatus) {
|
if (prevStatus) {
|
||||||
prevStatus.focus();
|
prevStatus.focus();
|
||||||
prevStatus.scrollIntoViewIfNeeded?.();
|
prevStatus.scrollIntoView(scrollIntoViewOptions);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// If active status is not in viewport, get the topmost status-link in viewport
|
// If active status is not in viewport, get the topmost status-link in viewport
|
||||||
|
@ -598,7 +608,7 @@ function StatusThread({ id, closeLink = '/', instance: propInstance }) {
|
||||||
});
|
});
|
||||||
if (topmostStatusLink) {
|
if (topmostStatusLink) {
|
||||||
topmostStatusLink.focus();
|
topmostStatusLink.focus();
|
||||||
topmostStatusLink.scrollIntoViewIfNeeded?.();
|
topmostStatusLink.scrollIntoView(scrollIntoViewOptions);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -939,12 +949,13 @@ function StatusThread({ id, closeLink = '/', instance: propInstance }) {
|
||||||
setUIState('loading');
|
setUIState('loading');
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
const results = await currentMasto.v2.search({
|
const results =
|
||||||
q: heroStatus.url,
|
await currentMasto.v2.search.fetch({
|
||||||
type: 'statuses',
|
q: heroStatus.url,
|
||||||
resolve: true,
|
type: 'statuses',
|
||||||
limit: 1,
|
resolve: true,
|
||||||
});
|
limit: 1,
|
||||||
|
});
|
||||||
if (results.statuses.length) {
|
if (results.statuses.length) {
|
||||||
const status = results.statuses[0];
|
const status = results.statuses[0];
|
||||||
location.hash = currentInstance
|
location.hash = currentInstance
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Menu, MenuItem } from '@szhsin/react-menu';
|
import { MenuItem } from '@szhsin/react-menu';
|
||||||
import { useMemo, useRef, useState } from 'preact/hooks';
|
import { useMemo, useRef, useState } from 'preact/hooks';
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
import { useSnapshot } from 'valtio';
|
import { useSnapshot } from 'valtio';
|
||||||
|
@ -30,13 +30,13 @@ function Trending({ columnMode, ...props }) {
|
||||||
const trendIterator = useRef();
|
const trendIterator = useRef();
|
||||||
async function fetchTrend(firstLoad) {
|
async function fetchTrend(firstLoad) {
|
||||||
if (firstLoad || !trendIterator.current) {
|
if (firstLoad || !trendIterator.current) {
|
||||||
trendIterator.current = masto.v1.trends.listStatuses({
|
trendIterator.current = masto.v1.trends.statuses.list({
|
||||||
limit: LIMIT,
|
limit: LIMIT,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Get hashtags
|
// Get hashtags
|
||||||
try {
|
try {
|
||||||
const iterator = masto.v1.trends.listTags();
|
const iterator = masto.v1.trends.tags.list();
|
||||||
const { value: tags } = await iterator.next();
|
const { value: tags } = await iterator.next();
|
||||||
console.log(tags);
|
console.log(tags);
|
||||||
setHashtags(tags);
|
setHashtags(tags);
|
||||||
|
@ -64,8 +64,8 @@ function Trending({ columnMode, ...props }) {
|
||||||
|
|
||||||
async function checkForUpdates() {
|
async function checkForUpdates() {
|
||||||
try {
|
try {
|
||||||
const results = await masto.v1.trends
|
const results = await masto.v1.trends.statuses
|
||||||
.listStatuses({
|
.list({
|
||||||
limit: 1,
|
limit: 1,
|
||||||
// NOT SUPPORTED
|
// NOT SUPPORTED
|
||||||
// since_id: latestItem.current,
|
// since_id: latestItem.current,
|
||||||
|
|
|
@ -7,6 +7,7 @@ import multiHashtagTimelineUrl from '../assets/features/multi-hashtag-timeline.j
|
||||||
import nestedCommentsThreadUrl from '../assets/features/nested-comments-thread.jpg';
|
import nestedCommentsThreadUrl from '../assets/features/nested-comments-thread.jpg';
|
||||||
import logoText from '../assets/logo-text.svg';
|
import logoText from '../assets/logo-text.svg';
|
||||||
import logo from '../assets/logo.svg';
|
import logo from '../assets/logo.svg';
|
||||||
|
|
||||||
import Link from '../components/link';
|
import Link from '../components/link';
|
||||||
import states from '../utils/states';
|
import states from '../utils/states';
|
||||||
import useTitle from '../utils/useTitle';
|
import useTitle from '../utils/useTitle';
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { createClient } from 'masto';
|
import { createRestAPIClient, createStreamingAPIClient } from 'masto';
|
||||||
|
|
||||||
import store from './store';
|
import store from './store';
|
||||||
import {
|
import {
|
||||||
|
@ -37,14 +37,17 @@ export function initClient({ instance, accessToken }) {
|
||||||
}
|
}
|
||||||
const url = instance ? `https://${instance}` : `https://${DEFAULT_INSTANCE}`;
|
const url = instance ? `https://${instance}` : `https://${DEFAULT_INSTANCE}`;
|
||||||
|
|
||||||
const client = createClient({
|
const masto = createRestAPIClient({
|
||||||
url,
|
url,
|
||||||
accessToken, // Can be null
|
accessToken, // Can be null
|
||||||
disableVersionCheck: true, // Allow non-Mastodon instances
|
|
||||||
timeout: 30_000, // Unfortunatly this is global instead of per-request
|
timeout: 30_000, // Unfortunatly this is global instead of per-request
|
||||||
});
|
});
|
||||||
client.__instance__ = instance;
|
|
||||||
|
|
||||||
|
const client = {
|
||||||
|
masto,
|
||||||
|
instance,
|
||||||
|
accessToken,
|
||||||
|
};
|
||||||
apis[instance] = client;
|
apis[instance] = client;
|
||||||
if (!accountApis[instance]) accountApis[instance] = {};
|
if (!accountApis[instance]) accountApis[instance] = {};
|
||||||
if (accessToken) accountApis[instance][accessToken] = client;
|
if (accessToken) accountApis[instance][accessToken] = client;
|
||||||
|
@ -55,7 +58,8 @@ export function initClient({ instance, accessToken }) {
|
||||||
// Get the instance information
|
// Get the instance information
|
||||||
// The config is needed for composing
|
// The config is needed for composing
|
||||||
export async function initInstance(client, instance) {
|
export async function initInstance(client, instance) {
|
||||||
const masto = client;
|
console.log('INIT INSTANCE', client, instance);
|
||||||
|
const { masto, accessToken } = client;
|
||||||
// Request v2, fallback to v1 if fail
|
// Request v2, fallback to v1 if fail
|
||||||
let info;
|
let info;
|
||||||
try {
|
try {
|
||||||
|
@ -63,7 +67,7 @@ export async function initInstance(client, instance) {
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
if (!info) {
|
if (!info) {
|
||||||
try {
|
try {
|
||||||
info = await masto.v1.instances.fetch();
|
info = await masto.v1.instance.fetch();
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
if (!info) return;
|
if (!info) return;
|
||||||
|
@ -91,17 +95,28 @@ export async function initInstance(client, instance) {
|
||||||
store.local.setJSON('instances', instances);
|
store.local.setJSON('instances', instances);
|
||||||
// This is a weird place to put this but here's updating the masto instance with the streaming API URL set in the configuration
|
// This is a weird place to put this but here's updating the masto instance with the streaming API URL set in the configuration
|
||||||
// Reason: Streaming WebSocket URL may change, unlike the standard API REST URLs
|
// Reason: Streaming WebSocket URL may change, unlike the standard API REST URLs
|
||||||
if (streamingApi || streaming) {
|
const supportsWebSocket = 'WebSocket' in window;
|
||||||
|
if (supportsWebSocket && (streamingApi || streaming)) {
|
||||||
console.log('🎏 Streaming API URL:', streaming || streamingApi);
|
console.log('🎏 Streaming API URL:', streaming || streamingApi);
|
||||||
masto.config.props.streamingApiUrl = streaming || streamingApi;
|
// masto.config.props.streamingApiUrl = streaming || streamingApi;
|
||||||
|
// Legacy masto.ws
|
||||||
|
const streamClient = createStreamingAPIClient({
|
||||||
|
streamingApiUrl: streaming || streamingApi,
|
||||||
|
accessToken,
|
||||||
|
implementation: WebSocket,
|
||||||
|
});
|
||||||
|
client.streaming = streamClient;
|
||||||
|
// masto.ws = streamClient;
|
||||||
|
console.log('🎏 Streaming API client:', client);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the account information and store it
|
// Get the account information and store it
|
||||||
export async function initAccount(client, instance, accessToken, vapidKey) {
|
export async function initAccount(client, instance, accessToken, vapidKey) {
|
||||||
const masto = client;
|
const { masto } = client;
|
||||||
const mastoAccount = await masto.v1.accounts.verifyCredentials();
|
const mastoAccount = await masto.v1.accounts.verifyCredentials();
|
||||||
|
|
||||||
|
console.log('CURRENTACCOUNT SET', mastoAccount.id);
|
||||||
store.session.set('currentAccount', mastoAccount.id);
|
store.session.set('currentAccount', mastoAccount.id);
|
||||||
|
|
||||||
saveAccount({
|
saveAccount({
|
||||||
|
@ -115,7 +130,7 @@ export async function initAccount(client, instance, accessToken, vapidKey) {
|
||||||
// Get preferences
|
// Get preferences
|
||||||
export async function initPreferences(client) {
|
export async function initPreferences(client) {
|
||||||
try {
|
try {
|
||||||
const masto = client;
|
const { masto } = client;
|
||||||
const preferences = await masto.v1.preferences.fetch();
|
const preferences = await masto.v1.preferences.fetch();
|
||||||
store.account.set('preferences', preferences);
|
store.account.set('preferences', preferences);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -134,10 +149,14 @@ export function api({ instance, accessToken, accountID, account } = {}) {
|
||||||
|
|
||||||
// If instance and accessToken are provided, get the masto instance for that account
|
// If instance and accessToken are provided, get the masto instance for that account
|
||||||
if (instance && accessToken) {
|
if (instance && accessToken) {
|
||||||
|
const client =
|
||||||
|
accountApis[instance]?.[accessToken] ||
|
||||||
|
initClient({ instance, accessToken });
|
||||||
|
const { masto, streaming } = client;
|
||||||
return {
|
return {
|
||||||
masto:
|
masto,
|
||||||
accountApis[instance]?.[accessToken] ||
|
streaming,
|
||||||
initClient({ instance, accessToken }),
|
client,
|
||||||
authenticated: true,
|
authenticated: true,
|
||||||
instance,
|
instance,
|
||||||
};
|
};
|
||||||
|
@ -149,8 +168,12 @@ export function api({ instance, accessToken, accountID, account } = {}) {
|
||||||
for (const instance in accountApis) {
|
for (const instance in accountApis) {
|
||||||
if (accountApis[instance][accessToken]) {
|
if (accountApis[instance][accessToken]) {
|
||||||
console.log('X 2', accountApis, instance, accessToken);
|
console.log('X 2', accountApis, instance, accessToken);
|
||||||
|
const client = accountApis[instance][accessToken];
|
||||||
|
const { masto, streaming } = client;
|
||||||
return {
|
return {
|
||||||
masto: accountApis[instance][accessToken],
|
masto,
|
||||||
|
streaming,
|
||||||
|
client,
|
||||||
authenticated: true,
|
authenticated: true,
|
||||||
instance,
|
instance,
|
||||||
};
|
};
|
||||||
|
@ -160,13 +183,17 @@ export function api({ instance, accessToken, accountID, account } = {}) {
|
||||||
if (account) {
|
if (account) {
|
||||||
const accessToken = account.accessToken;
|
const accessToken = account.accessToken;
|
||||||
const instance = account.instanceURL.toLowerCase().trim();
|
const instance = account.instanceURL.toLowerCase().trim();
|
||||||
|
const client = initClient({ instance, accessToken });
|
||||||
|
const { masto, streaming } = client;
|
||||||
return {
|
return {
|
||||||
masto: initClient({ instance, accessToken }),
|
masto,
|
||||||
|
streaming,
|
||||||
|
client,
|
||||||
authenticated: true,
|
authenticated: true,
|
||||||
instance,
|
instance,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`Access token ${accessToken} not found`);
|
throw new Error(`Access token not found`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -178,10 +205,14 @@ export function api({ instance, accessToken, accountID, account } = {}) {
|
||||||
if (account) {
|
if (account) {
|
||||||
const accessToken = account.accessToken;
|
const accessToken = account.accessToken;
|
||||||
const instance = account.instanceURL.toLowerCase().trim();
|
const instance = account.instanceURL.toLowerCase().trim();
|
||||||
|
const client =
|
||||||
|
accountApis[instance]?.[accessToken] ||
|
||||||
|
initClient({ instance, accessToken });
|
||||||
|
const { masto, streaming } = client;
|
||||||
return {
|
return {
|
||||||
masto:
|
masto,
|
||||||
accountApis[instance]?.[accessToken] ||
|
streaming,
|
||||||
initClient({ instance, accessToken }),
|
client,
|
||||||
authenticated: true,
|
authenticated: true,
|
||||||
instance,
|
instance,
|
||||||
};
|
};
|
||||||
|
@ -192,10 +223,13 @@ export function api({ instance, accessToken, accountID, account } = {}) {
|
||||||
|
|
||||||
// If only instance is provided, get the masto instance for that instance
|
// If only instance is provided, get the masto instance for that instance
|
||||||
if (instance) {
|
if (instance) {
|
||||||
const masto = apis[instance] || initClient({ instance });
|
const client = apis[instance] || initClient({ instance });
|
||||||
|
const { masto, streaming, accessToken } = client;
|
||||||
return {
|
return {
|
||||||
masto,
|
masto,
|
||||||
authenticated: !!masto.config.props.accessToken,
|
streaming,
|
||||||
|
client,
|
||||||
|
authenticated: !!accessToken,
|
||||||
instance,
|
instance,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -203,9 +237,11 @@ export function api({ instance, accessToken, accountID, account } = {}) {
|
||||||
// If no instance is provided, get the masto instance for the current account
|
// If no instance is provided, get the masto instance for the current account
|
||||||
if (currentAccountApi) {
|
if (currentAccountApi) {
|
||||||
return {
|
return {
|
||||||
masto: currentAccountApi,
|
masto: currentAccountApi.masto,
|
||||||
|
streaming: currentAccountApi.streaming,
|
||||||
|
client: currentAccountApi,
|
||||||
authenticated: true,
|
authenticated: true,
|
||||||
instance: currentAccountApi.__instance__,
|
instance: currentAccountApi.instance,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const currentAccount = getCurrentAccount();
|
const currentAccount = getCurrentAccount();
|
||||||
|
@ -215,15 +251,22 @@ export function api({ instance, accessToken, accountID, account } = {}) {
|
||||||
accountApis[instance]?.[accessToken] ||
|
accountApis[instance]?.[accessToken] ||
|
||||||
initClient({ instance, accessToken });
|
initClient({ instance, accessToken });
|
||||||
return {
|
return {
|
||||||
masto: currentAccountApi,
|
masto: currentAccountApi.masto,
|
||||||
|
streaming: currentAccountApi.streaming,
|
||||||
|
client: currentAccountApi,
|
||||||
authenticated: true,
|
authenticated: true,
|
||||||
instance,
|
instance,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// If no instance is provided and no account is logged in, get the masto instance for DEFAULT_INSTANCE
|
// If no instance is provided and no account is logged in, get the masto instance for DEFAULT_INSTANCE
|
||||||
|
const client =
|
||||||
|
apis[DEFAULT_INSTANCE] || initClient({ instance: DEFAULT_INSTANCE });
|
||||||
|
const { masto, streaming } = client;
|
||||||
return {
|
return {
|
||||||
masto: apis[DEFAULT_INSTANCE] || initClient({ instance: DEFAULT_INSTANCE }),
|
masto,
|
||||||
|
streaming,
|
||||||
|
client,
|
||||||
authenticated: false,
|
authenticated: false,
|
||||||
instance: DEFAULT_INSTANCE,
|
instance: DEFAULT_INSTANCE,
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
import emojifyText from './emojify-text';
|
import emojifyText from './emojify-text';
|
||||||
|
import mem from './mem';
|
||||||
|
|
||||||
const fauxDiv = document.createElement('div');
|
const fauxDiv = document.createElement('div');
|
||||||
const whitelistLinkClasses = ['u-url', 'mention', 'hashtag'];
|
const whitelistLinkClasses = ['u-url', 'mention', 'hashtag'];
|
||||||
|
|
||||||
function enhanceContent(content, opts = {}) {
|
function _enhanceContent(content, opts = {}) {
|
||||||
const { emojis, postEnhanceDOM = () => {} } = opts;
|
const { emojis, postEnhanceDOM = () => {} } = opts;
|
||||||
let enhancedContent = content;
|
let enhancedContent = content;
|
||||||
const dom = document.createElement('div');
|
const dom = document.createElement('div');
|
||||||
|
@ -250,6 +251,7 @@ function enhanceContent(content, opts = {}) {
|
||||||
|
|
||||||
return enhancedContent;
|
return enhancedContent;
|
||||||
}
|
}
|
||||||
|
const enhanceContent = mem(_enhanceContent);
|
||||||
|
|
||||||
const defaultRejectFilter = [
|
const defaultRejectFilter = [
|
||||||
// Document metadata
|
// Document metadata
|
||||||
|
|
|
@ -3,6 +3,11 @@ import translationTargetLanguages from '../data/lingva-target-languages';
|
||||||
import localeMatch from './locale-match';
|
import localeMatch from './locale-match';
|
||||||
import states from './states';
|
import states from './states';
|
||||||
|
|
||||||
|
const locales = [
|
||||||
|
new Intl.DateTimeFormat().resolvedOptions().locale,
|
||||||
|
...navigator.languages,
|
||||||
|
];
|
||||||
|
|
||||||
function getTranslateTargetLanguage(fromSettings = false) {
|
function getTranslateTargetLanguage(fromSettings = false) {
|
||||||
if (fromSettings) {
|
if (fromSettings) {
|
||||||
const { contentTranslationTargetLanguage } = states.settings;
|
const { contentTranslationTargetLanguage } = states.settings;
|
||||||
|
@ -11,10 +16,7 @@ function getTranslateTargetLanguage(fromSettings = false) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return localeMatch(
|
return localeMatch(
|
||||||
[
|
locales,
|
||||||
new Intl.DateTimeFormat().resolvedOptions().locale,
|
|
||||||
...navigator.languages,
|
|
||||||
],
|
|
||||||
translationTargetLanguages.map((l) => l.code.replace('_', '-')), // The underscore will fail Intl.Locale inside `match`
|
translationTargetLanguages.map((l) => l.code.replace('_', '-')), // The underscore will fail Intl.Locale inside `match`
|
||||||
'en',
|
'en',
|
||||||
);
|
);
|
||||||
|
|
|
@ -2,7 +2,7 @@ export default function isMastodonLinkMaybe(url) {
|
||||||
const { pathname } = new URL(url);
|
const { pathname } = new URL(url);
|
||||||
return (
|
return (
|
||||||
/^\/.*\/\d+$/i.test(pathname) ||
|
/^\/.*\/\d+$/i.test(pathname) ||
|
||||||
/^\/@[^/]+\/statuses\/\w+$/i.test(pathname) || // GoToSocial
|
/^\/@[^/]+\/(statuses|posts)\/\w+\/?$/i.test(pathname) || // GoToSocial, Takahe
|
||||||
/^\/notes\/[a-z0-9]+$/i.test(pathname) || // Misskey, Calckey
|
/^\/notes\/[a-z0-9]+$/i.test(pathname) || // Misskey, Calckey
|
||||||
/^\/(notice|objects)\/[a-z0-9-]+$/i.test(pathname) // Pleroma
|
/^\/(notice|objects)\/[a-z0-9-]+$/i.test(pathname) // Pleroma
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { match } from '@formatjs/intl-localematcher';
|
import { match } from '@formatjs/intl-localematcher';
|
||||||
import mem from 'mem';
|
|
||||||
|
import mem from './mem';
|
||||||
|
|
||||||
function _localeMatch(...args) {
|
function _localeMatch(...args) {
|
||||||
// Wrap in try/catch because localeMatcher throws on invalid locales
|
// Wrap in try/catch because localeMatcher throws on invalid locales
|
||||||
|
@ -10,8 +11,6 @@ function _localeMatch(...args) {
|
||||||
return defaultLocale || false;
|
return defaultLocale || false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const localeMatch = mem(_localeMatch, {
|
const localeMatch = mem(_localeMatch);
|
||||||
cacheKey: (args) => args.join(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export default localeMatch;
|
export default localeMatch;
|
||||||
|
|
5
src/utils/mem.js
Normal file
5
src/utils/mem.js
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import moize from 'moize';
|
||||||
|
|
||||||
|
export default function mem(fn, opts = {}) {
|
||||||
|
return moize(fn, { ...opts, maxSize: 100 });
|
||||||
|
}
|
5
src/utils/pmem.js
Normal file
5
src/utils/pmem.js
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import mem from './mem';
|
||||||
|
|
||||||
|
export default function pmem(fn, opts = {}) {
|
||||||
|
return mem(fn, { isPromise: true, ...opts });
|
||||||
|
}
|
|
@ -34,22 +34,22 @@ import { getCurrentAccount } from './store-utils';
|
||||||
|
|
||||||
function createBackendPushSubscription(subscription) {
|
function createBackendPushSubscription(subscription) {
|
||||||
const { masto } = api();
|
const { masto } = api();
|
||||||
return masto.v1.webPushSubscriptions.create(subscription);
|
return masto.v1.push.subscription.create(subscription);
|
||||||
}
|
}
|
||||||
|
|
||||||
function fetchBackendPushSubscription() {
|
function fetchBackendPushSubscription() {
|
||||||
const { masto } = api();
|
const { masto } = api();
|
||||||
return masto.v1.webPushSubscriptions.fetch();
|
return masto.v1.push.subscription.fetch();
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateBackendPushSubscription(subscription) {
|
function updateBackendPushSubscription(subscription) {
|
||||||
const { masto } = api();
|
const { masto } = api();
|
||||||
return masto.v1.webPushSubscriptions.update(subscription);
|
return masto.v1.push.subscription.update(subscription);
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeBackendPushSubscription() {
|
function removeBackendPushSubscription() {
|
||||||
const { masto } = api();
|
const { masto } = api();
|
||||||
return masto.v1.webPushSubscriptions.remove();
|
return masto.v1.push.subscription.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Front-end
|
// Front-end
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import mem from 'mem';
|
|
||||||
import { proxy, subscribe } from 'valtio';
|
import { proxy, subscribe } from 'valtio';
|
||||||
import { subscribeKey } from 'valtio/utils';
|
import { subscribeKey } from 'valtio/utils';
|
||||||
|
|
||||||
import { api } from './api';
|
import { api } from './api';
|
||||||
|
import pmem from './pmem';
|
||||||
import store from './store';
|
import store from './store';
|
||||||
|
|
||||||
const states = proxy({
|
const states = proxy({
|
||||||
|
@ -18,12 +18,16 @@ const states = proxy({
|
||||||
homeLast: null, // Last item in 'home' list
|
homeLast: null, // Last item in 'home' list
|
||||||
homeLastFetchTime: null,
|
homeLastFetchTime: null,
|
||||||
notifications: [],
|
notifications: [],
|
||||||
notificationsLast: store.account.get('notificationsLast') || null, // Last read notification
|
notificationsLast: null, // Last read notification
|
||||||
notificationsNew: [],
|
notificationsNew: [],
|
||||||
notificationsShowNew: false,
|
notificationsShowNew: false,
|
||||||
notificationsLastFetchTime: null,
|
notificationsLastFetchTime: null,
|
||||||
accounts: {},
|
accounts: {},
|
||||||
reloadStatusPage: 0,
|
reloadStatusPage: 0,
|
||||||
|
reloadGenericAccounts: {
|
||||||
|
id: null,
|
||||||
|
counter: 0,
|
||||||
|
},
|
||||||
spoilers: {},
|
spoilers: {},
|
||||||
scrollPositions: {},
|
scrollPositions: {},
|
||||||
unfurledLinks: {},
|
unfurledLinks: {},
|
||||||
|
@ -39,24 +43,21 @@ const states = proxy({
|
||||||
showMediaModal: false,
|
showMediaModal: false,
|
||||||
showShortcutsSettings: false,
|
showShortcutsSettings: false,
|
||||||
showKeyboardShortcutsHelp: false,
|
showKeyboardShortcutsHelp: false,
|
||||||
|
showGenericAccounts: false,
|
||||||
|
showMediaAlt: false,
|
||||||
// Shortcuts
|
// Shortcuts
|
||||||
shortcuts: store.account.get('shortcuts') ?? [],
|
shortcuts: [],
|
||||||
// Settings
|
// Settings
|
||||||
settings: {
|
settings: {
|
||||||
autoRefresh: store.account.get('settings-autoRefresh') ?? false,
|
autoRefresh: false,
|
||||||
shortcutsViewMode: store.account.get('settings-shortcutsViewMode') ?? null,
|
shortcutsViewMode: null,
|
||||||
shortcutsColumnsMode:
|
shortcutsColumnsMode: false,
|
||||||
store.account.get('settings-shortcutsColumnsMode') ?? false,
|
boostsCarousel: true,
|
||||||
boostsCarousel: store.account.get('settings-boostsCarousel') ?? true,
|
contentTranslation: true,
|
||||||
contentTranslation:
|
contentTranslationTargetLanguage: null,
|
||||||
store.account.get('settings-contentTranslation') ?? true,
|
contentTranslationHideLanguages: [],
|
||||||
contentTranslationTargetLanguage:
|
contentTranslationAutoInline: false,
|
||||||
store.account.get('settings-contentTranslationTargetLanguage') || null,
|
cloakMode: false,
|
||||||
contentTranslationHideLanguages:
|
|
||||||
store.account.get('settings-contentTranslationHideLanguages') || [],
|
|
||||||
contentTranslationAutoInline:
|
|
||||||
store.account.get('settings-contentTranslationAutoInline') ?? false,
|
|
||||||
cloakMode: store.account.get('settings-cloakMode') ?? false,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -140,6 +141,7 @@ export function hideAllModals() {
|
||||||
states.showShortcutsSettings = false;
|
states.showShortcutsSettings = false;
|
||||||
states.showKeyboardShortcutsHelp = false;
|
states.showKeyboardShortcutsHelp = false;
|
||||||
states.showGenericAccounts = false;
|
states.showGenericAccounts = false;
|
||||||
|
states.showMediaAlt = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function statusKey(id, instance) {
|
export function statusKey(id, instance) {
|
||||||
|
@ -205,7 +207,7 @@ export function threadifyStatus(status, propInstance) {
|
||||||
if (!prevStatus) {
|
if (!prevStatus) {
|
||||||
if (fetchIndex++ > 3) throw 'Too many fetches for thread'; // Some people revive old threads
|
if (fetchIndex++ > 3) throw 'Too many fetches for thread'; // Some people revive old threads
|
||||||
await new Promise((r) => setTimeout(r, 500 * fetchIndex)); // Be nice to rate limits
|
await new Promise((r) => setTimeout(r, 500 * fetchIndex)); // Be nice to rate limits
|
||||||
// prevStatus = await masto.v1.statuses.fetch(inReplyToId);
|
// prevStatus = await masto.v1.statuses.$.select(inReplyToId).fetch();
|
||||||
prevStatus = await fetchStatus(inReplyToId, masto);
|
prevStatus = await fetchStatus(inReplyToId, masto);
|
||||||
saveStatus(prevStatus, instance, { skipThreading: true });
|
saveStatus(prevStatus, instance, { skipThreading: true });
|
||||||
}
|
}
|
||||||
|
@ -227,6 +229,6 @@ export function threadifyStatus(status, propInstance) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchStatus = mem((statusID, masto) => {
|
const fetchStatus = pmem((statusID, masto) => {
|
||||||
return masto.v1.statuses.fetch(statusID);
|
return masto.v1.statuses.$select(statusID).fetch();
|
||||||
});
|
});
|
||||||
|
|
|
@ -11,6 +11,11 @@ export function getAccountByAccessToken(accessToken) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getCurrentAccount() {
|
export function getCurrentAccount() {
|
||||||
|
if (!window.__IGNORE_GET_ACCOUNT_ERROR__) {
|
||||||
|
// Track down getCurrentAccount() calls before account-based states are initialized
|
||||||
|
console.error('getCurrentAccount() called before states are initialized');
|
||||||
|
if (import.meta.env.DEV) console.trace();
|
||||||
|
}
|
||||||
const currentAccount = store.session.get('currentAccount');
|
const currentAccount = store.session.get('currentAccount');
|
||||||
const account = getAccount(currentAccount);
|
const account = getAccount(currentAccount);
|
||||||
return account;
|
return account;
|
||||||
|
|
|
@ -1,17 +1,25 @@
|
||||||
import { useRef } from 'preact/hooks';
|
import { useRef } from 'preact/hooks';
|
||||||
|
import { useThrottledCallback } from 'use-debounce';
|
||||||
import useResizeObserver from 'use-resize-observer';
|
import useResizeObserver from 'use-resize-observer';
|
||||||
|
|
||||||
export default function useTruncated({ className = 'truncated' } = {}) {
|
export default function useTruncated({ className = 'truncated' } = {}) {
|
||||||
const ref = useRef();
|
const ref = useRef();
|
||||||
|
const onResize = useThrottledCallback(({ height }) => {
|
||||||
|
if (ref.current) {
|
||||||
|
const { scrollHeight } = ref.current;
|
||||||
|
let truncated = scrollHeight > height;
|
||||||
|
if (truncated) {
|
||||||
|
const { height: _height, maxHeight } = getComputedStyle(ref.current);
|
||||||
|
const computedHeight = parseInt(maxHeight || _height, 10);
|
||||||
|
truncated = scrollHeight > computedHeight;
|
||||||
|
}
|
||||||
|
ref.current.classList.toggle(className, truncated);
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
useResizeObserver({
|
useResizeObserver({
|
||||||
ref,
|
ref,
|
||||||
box: 'border-box',
|
box: 'border-box',
|
||||||
onResize: ({ height }) => {
|
onResize,
|
||||||
if (ref.current) {
|
|
||||||
const { scrollHeight } = ref.current;
|
|
||||||
ref.current.classList.toggle(className, scrollHeight > height);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
return ref;
|
return ref;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue