Compare commits

..

No commits in common. "96f1a9d9f24e0a8d16754258c697858ef1fbbad4" and "9d813802a9bffeb08ba92172ebb9eddb216319b6" have entirely different histories.

65 changed files with 2189 additions and 2762 deletions

View file

@ -12,5 +12,5 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
ref: production ref: production
- run: git tag "`date +%Y.%m.%d`.`git rev-parse --short HEAD`" $(git rev-parse HEAD) - run: git tag -a "'{date +%Y.%m.%d}.{git rev-parse --short HEAD}'" $(git rev-parse HEAD)
- run: git push --tags - run: git push

View file

@ -1,25 +0,0 @@
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
View file

@ -26,7 +26,6 @@ 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

View file

@ -8,7 +8,6 @@
"index.css$", "index.css$",
".css$", ".css$",
"<THIRD_PARTY_MODULES>", "<THIRD_PARTY_MODULES>",
"/assets/",
"^../", "^../",
"^[./]" "^[./]"
], ],

View file

@ -121,7 +121,6 @@ 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.

View file

@ -5,11 +5,11 @@
"systems": "systems" "systems": "systems"
}, },
"locked": { "locked": {
"lastModified": 1694529238, "lastModified": 1689068808,
"narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=", "narHash": "sha256-6ixXo3wt24N/melDWjq70UuHQLxGV8jZvooRanIHXw0=",
"owner": "numtide", "owner": "numtide",
"repo": "flake-utils", "repo": "flake-utils",
"rev": "ff7b65b44d01cf9ba6a71320833626af21126384", "rev": "919d646de7be200f3bf08cb76ae1f09402b6f9b4",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -20,16 +20,16 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1697059129, "lastModified": 1689413807,
"narHash": "sha256-9NJcFF9CEYPvHJ5ckE8kvINvI84SZZ87PvqMbH6pro0=", "narHash": "sha256-exuzOvOhGAEKWQKwDuZAL4N8a1I837hH5eocaTcIbLc=",
"owner": "nixOS", "owner": "nixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "5e4c2ada4fcd54b99d56d7bd62f384511a7e2593", "rev": "46ed466081b9cad1125b11f11a2af5cc40b942c7",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "nixOS", "owner": "nixOS",
"ref": "nixos-unstable", "ref": "nixpkgs-unstable",
"repo": "nixpkgs", "repo": "nixpkgs",
"type": "github" "type": "github"
} }

View file

@ -1,5 +1,5 @@
{ {
inputs.nixpkgs.url = github:nixOS/nixpkgs/nixos-unstable; inputs.nixpkgs.url = github:nixOS/nixpkgs/nixpkgs-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-LpvZfIzIdgxXg4upcDKm7jbK7CjrRvg//HULO4GDTdU="; npmDepsHash = "sha256-tqR3YQ++nJmwDNKIm7uFLhJ5HlAqfeEmJVyynHx3Hzw=";
# npmDepsHash = lib.fakeHash; # npmDepsHash = lib.fakeHash;
# DTTH-specific env variables # DTTH-specific env variables
@ -33,7 +33,6 @@
}; };
devShells.default = pkgs.mkShell { devShells.default = pkgs.mkShell {
inputsFrom = [ packages.default ];
buildInputs = with pkgs; [ nodejs ]; buildInputs = with pkgs; [ nodejs ];
}; };
}); });

2326
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -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.8", "@iconify-icons/mingcute": "~1.2.7",
"@justinribeiro/lite-youtube": "~1.5.0", "@justinribeiro/lite-youtube": "~1.5.0",
"@szhsin/react-menu": "~4.1.0", "@szhsin/react-menu": "~4.0.3",
"@uidotdev/usehooks": "~2.4.0", "@uidotdev/usehooks": "~2.2.0",
"dayjs": "~1.11.10", "dayjs": "~1.11.9",
"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": "~6.3.1", "masto": "~5.11.4",
"moize": "~6.1.6", "mem": "~9.0.2",
"p-retry": "~6.1.0", "p-retry": "~6.0.0",
"p-throttle": "~5.1.0", "p-throttle": "~5.1.0",
"preact": "~10.18.1", "preact": "~10.17.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": "~5.0.0", "react-quick-pinch-zoom": "~4.9.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.6.0", "@preact/preset-vite": "~2.5.0",
"@trivago/prettier-plugin-sort-imports": "~4.2.0", "@trivago/prettier-plugin-sort-imports": "~4.2.0",
"postcss": "~8.4.31", "postcss": "~8.4.29",
"postcss-dark-theme-class": "~1.0.0", "postcss-dark-theme-class": "~1.0.0",
"postcss-preset-env": "~9.2.0", "postcss-preset-env": "~9.1.3",
"twitter-text": "~3.1.0", "twitter-text": "~3.1.0",
"vite": "~4.4.11", "vite": "~4.4.9",
"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",

View file

@ -161,23 +161,33 @@ 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 url = `/#/notifications?id=${tag}&access_token=${btoa(access_token)}`; const actions = new Promise((resolve) => {
event.notification.close(); event.notification.close();
event.waitUntil( const url = `/#/notifications?id=${tag}&access_token=${btoa(access_token)}`;
(async () => { self.clients
const clients = await self.clients.matchAll({ .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) =>
client.focused || client.visibilityState === 'visible',
) || clients[0]; ) || clients[0];
console.log('NOTIFICATION CLICK navigate', url); console.log('NOTIFICATION CLICK navigate', url);
// Check if URL is root / or /notifications
// const clientURL = new URL(bestClient.url);
// if (
// /^#\/?$/.test(clientURL.hash) ||
// /^#\/notifications/i.test(clientURL.hash)
// ) {
// 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) { if (bestClient) {
console.log('NOTIFICATION CLICK postMessage', bestClient); console.log('NOTIFICATION CLICK postMessage', bestClient);
bestClient.postMessage?.({ bestClient.postMessage?.({
@ -188,13 +198,15 @@ self.addEventListener('notificationclick', (event) => {
bestClient.focus(); bestClient.focus();
} else { } else {
console.log('NOTIFICATION CLICK openWindow', url); console.log('NOTIFICATION CLICK openWindow', url);
await self.clients.openWindow(url); self.clients.openWindow(url);
} }
// } // }
} else { } else {
console.log('NOTIFICATION CLICK openWindow', url); console.log('NOTIFICATION CLICK openWindow', url);
await self.clients.openWindow(url); self.clients.openWindow(url);
} }
})(), resolve();
); });
});
event.waitUntil(actions);
}); });

View file

@ -1087,7 +1087,6 @@ 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);
@ -1120,10 +1119,11 @@ button.carousel-dot {
button.carousel-dot { button.carousel-dot {
background-color: transparent; background-color: transparent;
} }
:is(.button, button).carousel-button { .carousel-controls :is(.button, button).carousel-button {
background-color: var(--bg-blur-color); background-color: var(--bg-blur-color);
} }
:is(.button, button).carousel-button:is(:hover, :focus):not(:active) { .carousel-controls
: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,19 +1140,15 @@ 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);
@ -1335,16 +1331,12 @@ 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;
@ -1431,10 +1423,6 @@ 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;
@ -1754,7 +1742,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: 2px solid var(--link-faded-color); border: 1px solid var(--outline-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);
} }
@ -1762,7 +1750,8 @@ 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 3px 16px var(--drop-shadow-color), box-shadow: 0 0 0 1px var(--link-text-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);
} }

View file

@ -114,31 +114,28 @@ function App() {
code, code,
}); });
const client = initClient({ instance: instanceURL, accessToken }); const masto = initClient({ instance: instanceURL, accessToken });
await Promise.allSettled([ await Promise.allSettled([
initInstance(client, instanceURL), initInstance(masto, instanceURL),
initAccount(client, instanceURL, accessToken, vapidKey), initAccount(masto, instanceURL, accessToken, vapidKey),
]); ]);
initStates(); initStates();
initPreferences(client); initPreferences(masto);
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 { client } = api({ account }); const { masto, instance } = api({ account });
const { instance } = client; console.log('masto', masto);
// console.log('masto', masto); initPreferences(masto);
initStates();
initPreferences(client);
setUIState('loading'); setUIState('loading');
(async () => { (async () => {
try { try {
await initInstance(client, instance); await initInstance(masto, instance);
} catch (e) { } catch (e) {
} finally { } finally {
setIsLoggedIn(true); setIsLoggedIn(true);
@ -254,9 +251,9 @@ function App() {
<Shortcuts /> <Shortcuts />
)} )}
<Modals /> <Modals />
{isLoggedIn && <NotificationService />} <NotificationService />
<BackgroundService isLoggedIn={isLoggedIn} /> <BackgroundService isLoggedIn={isLoggedIn} />
{uiState !== 'loading' && <SearchCommand onClose={focusDeck} />} <SearchCommand onClose={focusDeck} />
<KeyboardShortcutsHelp /> <KeyboardShortcutsHelp />
</> </>
); );

View file

@ -3,6 +3,7 @@ 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';

View file

@ -220,7 +220,6 @@
} }
.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;
@ -343,82 +342,23 @@
opacity: 1; opacity: 1;
} }
} }
.account-container .posting-stats-button { .account-container .posting-stats {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
width: 100%;
color: inherit;
background-color: var(--bg-faded-color);
padding: 8px 12px;
font-size: 90%; font-size: 90%;
color: var(--text-insignificant-color); color: var(--text-insignificant-color);
line-height: 1; background-color: var(--bg-faded-color);
vertical-align: text-top; padding: 8px 12px;
border-radius: 4px; --size: 8px;
&:is(:hover, :focus-within) {
color: var(--text-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); --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) { &:is(:hover, :focus-within) {
background-color: var(--link-bg-hover-color); 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(--posting-stats-size); height: var(--size);
border-radius: var(--posting-stats-size); border-radius: var(--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),
@ -448,9 +388,9 @@
.posting-stats-legend-item { .posting-stats-legend-item {
display: inline-block; display: inline-block;
width: var(--posting-stats-size); width: var(--size);
height: var(--posting-stats-size); height: var(--size);
border-radius: var(--posting-stats-size); border-radius: var(--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;

View file

@ -1,21 +1,14 @@
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 { import { useEffect, useMemo, useReducer, useRef, useState } from 'preact/hooks';
useCallback, import { proxy, useSnapshot } from 'valtio';
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';
@ -56,64 +49,8 @@ const MUTE_DURATIONS_LABELS = {
const LIMIT = 80; const LIMIT = 80;
const ACCOUNT_INFO_MAX_AGE = 1000 * 60 * 10; // 10 mins const accountInfoStates = proxy({
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({
@ -126,10 +63,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'),
@ -184,7 +121,6 @@ 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 || {};
@ -198,19 +134,13 @@ 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.$select(id).followers.list({ followersIterator.current = masto.v1.accounts.listFollowers(id, {
limit: LIMIT, limit: LIMIT,
}); });
} }
@ -223,9 +153,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 const familiarFollowers = await masto.v1.accounts.fetchFamiliarFollowers(
.familiarFollowers(id) id,
.fetch(); );
familiarFollowersCache.current = familiarFollowers[0].accounts; familiarFollowersCache.current = familiarFollowers[0].accounts;
newValue = [ newValue = [
...familiarFollowersCache.current, ...familiarFollowersCache.current,
@ -254,7 +184,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.$select(id).following.list({ followingIterator.current = masto.v1.accounts.listFollowing(id, {
limit: LIMIT, limit: LIMIT,
}); });
} }
@ -265,51 +195,6 @@ 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' : ''}`}
@ -344,7 +229,7 @@ function AccountInfo({
<p> </p> <p> </p>
<p> </p> <p> </p>
</div> </div>
<div class="stats"> <p class="stats">
<div> <div>
<span></span> Followers <span></span> Followers
</div> </div>
@ -355,7 +240,7 @@ function AccountInfo({
<span></span> Posts <span></span> Posts
</div> </div>
<div>Joined </div> <div>Joined </div>
</div> </p>
</main> </main>
</> </>
) : ( ) : (
@ -496,20 +381,8 @@ 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,
})} })}
@ -526,7 +399,6 @@ 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} />{' '}
@ -555,17 +427,19 @@ function AccountInfo({
}; };
}} }}
> >
{!!familiarFollowers.length && ( {!!snapAccountInfoStates.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">
{familiarFollowers.map((follower) => ( {(snapAccountInfoStates.familiarFollowers || []).map(
(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>
)} )}
@ -620,112 +494,11 @@ 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}
onRelationshipChange={onRelationshipChange} standalone={standalone}
/> />
</main> </main>
</> </>
@ -737,12 +510,7 @@ function AccountInfo({
const FAMILIAR_FOLLOWERS_LIMIT = 3; const FAMILIAR_FOLLOWERS_LIMIT = 3;
function RelatedActions({ function RelatedActions({ info, instance, authenticated, standalone }) {
info,
instance,
authenticated,
onRelationshipChange = () => {},
}) {
if (!info) return null; if (!info) return null;
const { const {
masto: currentMasto, masto: currentMasto,
@ -753,6 +521,7 @@ function RelatedActions({
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;
@ -786,7 +555,7 @@ function RelatedActions({
// 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.fetch({ const results = await currentMasto.v2.search({
q: acctHasInstance ? info.acct : `${info.username}@${instance}`, q: acctHasInstance ? info.acct : `${info.username}@${instance}`,
type: 'accounts', type: 'accounts',
limit: 1, limit: 1,
@ -815,12 +584,12 @@ function RelatedActions({
if (moved) return; if (moved) return;
setRelationshipUIState('loading'); setRelationshipUIState('loading');
accountInfoStates.familiarFollowers = [];
setPostingStats(null);
const fetchRelationships = currentMasto.v1.accounts.relationships.fetch( const fetchRelationships = currentMasto.v1.accounts.fetchRelationships([
{ currentID,
id: [currentID], ]);
},
);
try { try {
const relationships = await fetchRelationships; const relationships = await fetchRelationships;
@ -830,7 +599,63 @@ function RelatedActions({
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);
@ -852,9 +677,75 @@ function RelatedActions({
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 (
<> <>
<div class="actions"> {hasPostingStats && (
<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>
@ -989,15 +880,14 @@ function RelatedActions({
setRelationshipUIState('loading'); setRelationshipUIState('loading');
(async () => { (async () => {
try { try {
const newRelationship = await currentMasto.v1.accounts const newRelationship =
.$select(currentInfo?.id || id) await currentMasto.v1.accounts.unmute(
.unmute(); currentInfo?.id || id,
);
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');
@ -1037,19 +927,18 @@ function RelatedActions({
(async () => { (async () => {
try { try {
const newRelationship = const newRelationship =
await currentMasto.v1.accounts await currentMasto.v1.accounts.mute(
.$select(currentInfo?.id || id) 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');
@ -1082,24 +971,24 @@ function RelatedActions({
(async () => { (async () => {
try { try {
if (blocking) { if (blocking) {
const newRelationship = await currentMasto.v1.accounts const newRelationship =
.$select(currentInfo?.id || id) await currentMasto.v1.accounts.unblock(
.unblock(); currentInfo?.id || id,
);
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 = await currentMasto.v1.accounts const newRelationship =
.$select(currentInfo?.id || id) await currentMasto.v1.accounts.block(
.block(); currentInfo?.id || id,
);
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');
@ -1161,14 +1050,14 @@ function RelatedActions({
// ); // );
// if (yes) { // if (yes) {
newRelationship = await currentMasto.v1.accounts newRelationship = await currentMasto.v1.accounts.unfollow(
.$select(accountID.current) accountID.current,
.unfollow(); );
// } // }
} else { } else {
newRelationship = await currentMasto.v1.accounts newRelationship = await currentMasto.v1.accounts.follow(
.$select(accountID.current) accountID.current,
.follow(); );
} }
if (newRelationship) setRelationship(newRelationship); if (newRelationship) setRelationship(newRelationship);
@ -1207,7 +1096,7 @@ function RelatedActions({
</MenuConfirm> </MenuConfirm>
)} )}
</span> </span>
</div> </p>
{!!showTranslatedBio && ( {!!showTranslatedBio && (
<Modal <Modal
class="light" class="light"
@ -1313,9 +1202,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 const listsContainingAccount = await masto.v1.accounts.listLists(
.$select(accountID) accountID,
.lists.list(); );
console.log({ lists, listsContainingAccount }); console.log({ lists, listsContainingAccount });
setLists(lists); setLists(lists);
setListsContainingAccount(listsContainingAccount); setListsContainingAccount(listsContainingAccount);
@ -1357,15 +1246,11 @@ function AddRemoveListsSheet({ accountID, onClose }) {
(async () => { (async () => {
try { try {
if (inList) { if (inList) {
await masto.v1.lists await masto.v1.lists.removeAccount(list.id, {
.$select(list.id)
.accounts.remove({
accountIds: [accountID], accountIds: [accountID],
}); });
} else { } else {
await masto.v1.lists await masto.v1.lists.addAccount(list.id, {
.$select(list.id)
.accounts.create({
accountIds: [accountID], accountIds: [accountID],
}); });
} }

View file

@ -46,7 +46,7 @@ function AccountSheet({ account, instance: propInstance, onClose }) {
}); });
return info; return info;
} catch (e) { } catch (e) {
const result = await masto.v2.search.fetch({ const result = await masto.v2.search({
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.fetch({ const result = await masto.v2.search({
q: acct, q: acct,
type: 'accounts', type: 'accounts',
limit: 1, limit: 1,

View file

@ -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, streaming, instance } = api(); const { masto, instance } = api();
(async () => { (async () => {
// 1. Get the latest notification // 1. Get the latest notification
if (states.notificationsLast) { if (states.notificationsLast) {
@ -42,26 +42,34 @@ export default memo(function BackgroundService({ isLoggedIn }) {
} }
// 2. Start streaming // 2. Start streaming
if (streaming) { notificationStream.current = await masto.ws.stream(
sub = streaming.user.notification.subscribe(); '/api/v1/streaming',
console.log('🎏 Streaming notification', sub); {
for await (const entry of sub) { stream: 'user:notification',
if (!sub) break; },
console.log('🔔🔔 Notification entry', entry); );
if (entry.event === 'notification') { console.log('🎏 Streaming notification', notificationStream.current);
console.log('🔔🔔 Notification', entry);
saveStatus(entry.payload, instance, { notificationStream.current.on('notification', (notification) => {
console.log('🔔🔔 Notification', notification);
if (notification.status) {
saveStatus(notification.status, instance, {
skipThreading: true, skipThreading: true,
}); });
} }
states.notificationsShowNew = true; states.notificationsShowNew = true;
} });
}
notificationStream.current.ws.onclose = () => {
console.log('🔔🔔 Notification stream closed');
};
})(); })();
} }
return () => { return () => {
sub?.unsubscribe?.(); if (notificationStream.current) {
sub = null; notificationStream.current.ws.close();
notificationStream.current = null;
}
}; };
}, [visible, isLoggedIn]); }, [visible, isLoggedIn]);

View file

@ -487,28 +487,7 @@
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;
@ -516,6 +495,10 @@
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;
@ -532,7 +515,6 @@
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%;
@ -552,11 +534,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 {

View file

@ -185,7 +185,7 @@ function Compose({
: visibility, : visibility,
); );
setLanguage(language || prefs.postingDefaultLanguage || DEFAULT_LANG); setLanguage(language || prefs.postingDefaultLanguage || DEFAULT_LANG);
setSensitive(sensitive && !!spoilerText); setSensitive(sensitive);
} 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 const statusSource = await masto.v1.statuses.fetchSource(
.$select(editStatus.id) 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,7 +749,9 @@ function Compose({
file, file,
description, description,
}); });
return masto.v2.media.create(params).then((res) => { return masto.v2.mediaAttachments
.create(params)
.then((res) => {
if (res.id) { if (res.id) {
attachment.id = res.id; attachment.id = res.id;
} }
@ -782,8 +784,6 @@ 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,9 +818,10 @@ function Compose({
let newStatus; let newStatus;
if (editStatus) { if (editStatus) {
newStatus = await masto.v1.statuses newStatus = await masto.v1.statuses.update(
.$select(editStatus.id) editStatus.id,
.update(params); params,
);
saveStatus(newStatus, instance, { saveStatus(newStatus, instance, {
skipThreading: true, skipThreading: true,
}); });
@ -838,8 +839,6 @@ function Compose({
// Close // Close
onClose({ onClose({
// type: post, reply, edit
type: editStatus ? 'edit' : replyToStatus ? 'reply' : 'post',
newStatus, newStatus,
instance, instance,
}); });
@ -934,13 +933,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.list({ return masto.v1.accounts.search({
q, q,
limit, limit,
resolve: false, resolve: false,
}); });
} }
return masto.v2.search.fetch(params); return masto.v2.search(params);
}} }}
/> />
{mediaAttachments?.length > 0 && ( {mediaAttachments?.length > 0 && (
@ -1476,11 +1475,7 @@ function MediaAttachment({
onRemove = () => {}, onRemove = () => {},
}) { }) {
const supportsEdit = supports('@mastodon/edit-media-attributes'); const supportsEdit = supports('@mastodon/edit-media-attributes');
const { type, id, file } = attachment; const { url, type, id } = 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];
@ -1547,7 +1542,6 @@ 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);
}} }}
@ -1574,7 +1568,6 @@ 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);
@ -1612,20 +1605,7 @@ function MediaAttachment({
<audio src={url} controls /> <audio src={url} controls />
) : null} ) : null}
</div> </div>
<div class="media-form">
{descTextarea} {descTextarea}
<footer>
<button
type="button"
class="light block"
onClick={() => {
setShowModal(false);
}}
>
Done
</button>
</footer>
</div>
</main> </main>
</div> </div>
</Modal> </Modal>

View file

@ -128,9 +128,9 @@ function Drafts({ onClose }) {
if (replyTo) { if (replyTo) {
setUIState('loading'); setUIState('loading');
try { try {
replyToStatus = await masto.v1.statuses replyToStatus = await masto.v1.statuses.fetch(
.$select(replyTo.id) 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!');

View file

@ -17,15 +17,13 @@ function FollowRequestButtons({ accountID, onChange }) {
<p class="follow-request-buttons"> <p class="follow-request-buttons">
<button <button
type="button" type="button"
disabled={uiState === 'loading' || hasRelationship} disabled={uiState === 'loading'}
onClick={() => { onClick={() => {
setUIState('loading'); setUIState('loading');
setRequestState('accept'); setRequestState('accept');
(async () => { (async () => {
try { try {
const rel = await masto.v1.followRequests const rel = await masto.v1.followRequests.authorize(accountID);
.$select(accountID)
.authorize();
if (!rel?.followedBy) { if (!rel?.followedBy) {
throw new Error('Follow request not accepted'); throw new Error('Follow request not accepted');
} }
@ -42,16 +40,14 @@ function FollowRequestButtons({ accountID, onChange }) {
</button>{' '} </button>{' '}
<button <button
type="button" type="button"
disabled={uiState === 'loading' || hasRelationship} disabled={uiState === 'loading'}
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 const rel = await masto.v1.followRequests.reject(accountID);
.$select(accountID)
.reject();
if (rel?.followedBy) { if (rel?.followedBy) {
throw new Error('Follow request not rejected'); throw new Error('Follow request not rejected');
} }

View file

@ -21,7 +21,6 @@ export default function GenericAccounts({ onClose = () => {} }) {
} }
const { const {
id,
heading, heading,
fetchAccounts, fetchAccounts,
accounts: staticAccounts, accounts: staticAccounts,
@ -61,14 +60,6 @@ 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}>

View file

@ -100,7 +100,6 @@ 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({
@ -127,7 +126,7 @@ function Icon({
}, [iconBlock]); }, [iconBlock]);
return ( return (
<span <div
class={`icon ${className}`} class={`icon ${className}`}
title={title || alt} title={title || alt}
style={{ style={{
@ -152,7 +151,7 @@ function Icon({
}} }}
/> />
)} )}
</span> </div>
); );
} }

View file

@ -56,7 +56,7 @@ function ListAddEdit({ list, onClose }) {
let listResult; let listResult;
if (editMode) { if (editMode) {
listResult = await masto.v1.lists.$select(list.id).update({ listResult = await masto.v1.lists.update(list.id, {
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.$select(list.id).remove(); await masto.v1.lists.remove(list.id);
setUIState('default'); setUIState('default');
onClose?.({ onClose?.({
state: 'deleted', state: 'deleted',

View file

@ -1,74 +0,0 @@
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>
);
}

View file

@ -1,4 +1,4 @@
import { Menu } from '@szhsin/react-menu'; import { Menu, MenuItem } 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,15 +6,14 @@ 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 = () => {},
}) { }) {
@ -139,19 +138,14 @@ function MediaModal({
class="media-alt" class="media-alt"
hidden={!showControls} hidden={!showControls}
onClick={() => { onClick={() => {
setShowMediaAlt({ setShowMediaAlt(media.description);
alt: media.description,
lang,
});
}} }}
> >
<span class="alt-badge">ALT</span> <Icon icon="info" />
<span class="media-alt-desc" lang={lang} dir="auto"> <span class="media-alt-desc">{media.description}</span>
{media.description}
</span>
</button> </button>
)} )}
<Media media={media} showOriginal lang={lang} /> <Media media={media} showOriginal />
</div> </div>
); );
})} })}
@ -285,8 +279,7 @@ function MediaModal({
}} }}
> >
<MediaAltModal <MediaAltModal
alt={showMediaAlt.alt || showMediaAlt} alt={showMediaAlt}
lang={showMediaAlt?.lang}
onClose={() => setShowMediaAlt(false)} onClose={() => setShowMediaAlt(false)}
/> />
</Modal> </Modal>
@ -295,4 +288,52 @@ 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;

View file

@ -9,9 +9,6 @@ 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';
@ -28,49 +25,7 @@ video = Video clip
audio = Audio track audio = Audio track
*/ */
const dataAltLabel = 'ALT'; function Media({ media, to, showOriginal, autoAnimate, onClick = () => {} }) {
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,
@ -179,35 +134,6 @@ function Media({
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';
@ -226,13 +152,11 @@ function Media({
}, [mediaURL]); }, [mediaURL]);
return ( return (
<Figure>
<Parent <Parent
ref={parentRef} ref={parentRef}
class={`media media-image`} class={`media media-image`}
onClick={onClick} onClick={onClick}
data-orientation={orientation} data-orientation={orientation}
data-has-alt={!showInlineDesc}
style={ style={
showOriginal showOriginal
? { ? {
@ -269,10 +193,9 @@ function Media({
/> />
</QuickPinchZoom> </QuickPinchZoom>
) : ( ) : (
<>
<img <img
src={mediaURL} src={mediaURL}
alt={showInlineDesc ? '' : description} alt={description}
width={width} width={width}
height={height} height={height}
data-orientation={orientation} data-orientation={orientation}
@ -300,13 +223,8 @@ function Media({
} }
}} }}
/> />
{!showInlineDesc && (
<AltBadge alt={description} lang={lang} index={altIndex} />
)}
</>
)} )}
</Parent> </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;
@ -334,8 +252,11 @@ function Media({
></video> ></video>
`; `;
const showInlineDesc = !showOriginal && !isGIF && !!description;
const Container = showInlineDesc ? 'figure' : Fragment;
return ( return (
<Figure> <Container>
<Parent <Parent
class={`media media-${isGIF ? 'gif' : 'video'} ${ class={`media media-${isGIF ? 'gif' : 'video'} ${
autoGIFAnimate ? 'media-contain' : '' autoGIFAnimate ? 'media-contain' : ''
@ -343,7 +264,6 @@ function Media({
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(',')})`,
@ -371,20 +291,6 @@ function Media({
} 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 ? (
@ -433,20 +339,24 @@ function Media({
</div> </div>
</> </>
)} )}
{!showOriginal && !showInlineDesc && (
<AltBadge alt={description} lang={lang} index={altIndex} />
)}
</Parent> </Parent>
</Figure> {showInlineDesc && (
<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 (
<Figure>
<Parent <Parent
class="media media-audio" class="media media-audio"
data-formatted-duration={formattedDuration} data-formatted-duration={formattedDuration}
data-has-alt={!showInlineDesc}
onClick={onClick} onClick={onClick}
style={!showOriginal && mediaStyles} style={!showOriginal && mediaStyles}
> >
@ -455,7 +365,7 @@ function Media({
) : previewUrl ? ( ) : previewUrl ? (
<img <img
src={previewUrl} src={previewUrl}
alt={showInlineDesc ? '' : description} alt={description}
width={width} width={width}
height={height} height={height}
data-orientation={orientation} data-orientation={orientation}
@ -463,17 +373,11 @@ function Media({
/> />
) : null} ) : null}
{!showOriginal && ( {!showOriginal && (
<>
<div class="media-play"> <div class="media-play">
<Icon icon="play" size="xl" /> <Icon icon="play" size="xl" />
</div> </div>
{!showInlineDesc && (
<AltBadge alt={description} lang={lang} index={altIndex} />
)}
</>
)} )}
</Parent> </Parent>
</Figure>
); );
} }
} }

View file

@ -11,7 +11,6 @@ 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';
@ -51,17 +50,13 @@ export default function Modals() {
null null
} }
onClose={(results) => { onClose={(results) => {
const { newStatus, instance, type } = results || {}; const { newStatus, instance } = results || {};
states.showCompose = false; states.showCompose = false;
window.__COMPOSE__ = null; window.__COMPOSE__ = null;
if (newStatus) { if (newStatus) {
states.reloadStatusPage++; states.reloadStatusPage++;
showToast({ showToast({
text: { text: 'Post published. Check it out.',
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) => {
@ -179,24 +174,6 @@ 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>
)}
</> </>
); );
} }

View file

@ -25,20 +25,13 @@ 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"
const shortenedAlphaNumericDisplayName = shortenedDisplayName.replace( .replace(/[^a-z0-9]/gi, ''); // Remove non-alphanumeric characters
/[^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;
} }

View file

@ -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 { masto, instance, authenticated } = api(); const { instance, authenticated } = api();
const [currentAccount, setCurrentAccount] = useState(); const [currentAccount, setCurrentAccount] = useState();
const [moreThanOneAccount, setMoreThanOneAccount] = useState(false); const [moreThanOneAccount, setMoreThanOneAccount] = useState(false);
@ -61,28 +61,6 @@ 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
@ -231,29 +209,6 @@ function NavMenu(props) {
> >
<Icon icon="group" size="l" /> <span>Accounts&hellip;</span> <Icon icon="group" size="l" /> <span>Accounts&hellip;</span>
</MenuItem> </MenuItem>
<MenuItem
onClick={() => {
states.showGenericAccounts = {
id: 'mute',
heading: 'Muted users',
fetchAccounts: fetchMutes,
};
}}
>
<Icon icon="mute" size="l" /> Muted users&hellip;
</MenuItem>
<MenuItem
onClick={() => {
states.showGenericAccounts = {
id: 'block',
heading: 'Blocked users',
fetchAccounts: fetchBlocks,
};
}}
>
<Icon icon="block" size="l" />
Blocked users&hellip;
</MenuItem>
<MenuItem <MenuItem
onClick={() => { onClick={() => {
states.showKeyboardShortcutsHelp = true; states.showKeyboardShortcutsHelp = true;

View file

@ -38,7 +38,7 @@ export default memo(function NotificationService() {
? getAccountByAccessToken(accessToken) ? getAccountByAccessToken(accessToken)
: getCurrentAccount(); : getCurrentAccount();
(async () => { (async () => {
const notification = await masto.v1.notifications.$select(id).fetch(); const notification = await masto.v1.notifications.fetch(id);
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;

View file

@ -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.sign_up': 'signed up.', 'admin.signup': 'signed up.',
'admin.report': (targetAccount) => <>reported {targetAccount}</>, 'admin.report': 'reported a post.',
}; };
const AVATARS_LIMIT = 50; const AVATARS_LIMIT = 50;
function Notification({ notification, instance, reload, isStatic }) { function Notification({ notification, instance, reload, isStatic }) {
const { id, status, account, report, _accounts, _statuses } = notification; const { id, status, account, _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,15 +119,7 @@ function Notification({ notification, instance, reload, isStatic }) {
} }
if (typeof text === 'function') { if (typeof text === 'function') {
const count = _statuses?.length || _accounts?.length; text = text(_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) {

View file

@ -199,7 +199,6 @@ export default function Poll({
setUIState('default'); setUIState('default');
})(); })();
}} }}
title="Refresh"
> >
<Icon icon="refresh" alt="Refresh" /> <Icon icon="refresh" alt="Refresh" />
</button> </button>
@ -213,7 +212,6 @@ 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'}

View file

@ -4,15 +4,14 @@ 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';
@ -133,10 +132,15 @@ export const SHORTCUTS_META = {
}, },
list: { list: {
id: 'list', id: 'list',
title: pmem(async ({ id }) => { title: mem(
const list = await api().masto.v1.lists.$select(id).fetch(); async ({ id }) => {
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',
}, },
@ -162,10 +166,15 @@ export const SHORTCUTS_META = {
}, },
'account-statuses': { 'account-statuses': {
id: 'account-statuses', id: 'account-statuses',
title: pmem(async ({ id }) => { title: mem(
const account = await api().masto.v1.accounts.$select(id).fetch(); async ({ id }) => {
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',
}, },

View file

@ -82,8 +82,6 @@
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;
@ -97,13 +95,7 @@
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: 100%; width: 20vw;
@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);
@ -179,8 +171,6 @@ 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;

View file

@ -166,11 +166,8 @@
.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, .poll, .media-container) {
font-size: inherit !important;
}
.status-card :is(.content.truncated, .poll, .media-container.truncated) { .status-card :is(.content.truncated, .poll, .media-container.truncated) {
/* font-size: inherit !important; */ 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
@ -302,7 +299,7 @@
overflow: hidden; overflow: hidden;
/* text-overflow: ellipsis; */ /* text-overflow: ellipsis; */
} }
.status > .container > .meta .meta-name { .status > .container > .meta .name-text {
mask-image: linear-gradient(to left, transparent, black 16px); mask-image: linear-gradient(to left, transparent, black 16px);
flex-grow: 1; flex-grow: 1;
} }
@ -337,7 +334,7 @@
.status > .container > .meta a.time:after { .status > .container > .meta a.time:after {
content: ''; content: '';
position: absolute; position: absolute;
inset: -16px -16px -8px; inset: -16px;
} }
.status > .container > .meta .reply-to { .status > .container > .meta .reply-to {
opacity: 0.5; opacity: 0.5;
@ -460,7 +457,7 @@
.status .status
.content-container.has-spoiler:not(.show-spoiler) .content-container.has-spoiler:not(.show-spoiler)
.spoiler .spoiler
~ *:not(.media-container, .card, .media-figure-multiple), ~ *:not(.media-container, .card),
.status .status
.content-container.has-spoiler:not(.show-spoiler) .content-container.has-spoiler:not(.show-spoiler)
.spoiler .spoiler
@ -469,7 +466,7 @@
.status .status
.content-container.has-spoiler:not(.show-spoiler) .content-container.has-spoiler:not(.show-spoiler)
.spoiler .spoiler
~ :is(.media-container, .media-figure-multiple) ~ .media-container
figcaption { figcaption {
filter: blur(5px) invert(0.5); filter: blur(5px) invert(0.5);
image-rendering: crisp-edges; image-rendering: crisp-edges;
@ -483,7 +480,7 @@
.status .status
.content-container.has-spoiler:not(.show-spoiler) .content-container.has-spoiler:not(.show-spoiler)
.spoiler .spoiler
~ :is(.media-container, .media-figure-multiple) ~ .media-container
.media .media
> *, > *,
.status .status
@ -547,7 +544,7 @@
max-height: 40vh; max-height: 40vh;
max-height: 40dvh; max-height: 40dvh;
} }
.timeline-deck .status:not(.truncated .status) .content.truncated { .timeline-deck .status .content.truncated {
mask-image: linear-gradient( mask-image: linear-gradient(
to top, to top,
transparent, transparent,
@ -555,7 +552,7 @@
black 1.5em black 1.5em
); );
} }
.timeline-deck .status:not(.truncated .status) .content.truncated:after { .timeline-deck .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;
@ -711,21 +708,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 {
align-self: flex-end; margin: -2px 0 0;
padding: 4px; padding: 0 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;
} }
} }
@ -836,7 +833,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:not(:has(button:active)) { .status .media:active {
filter: brightness(0.8); filter: brightness(0.8);
transform: scale(0.99); transform: scale(0.99);
} }
@ -848,22 +845,6 @@
} }
.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 {
@ -893,9 +874,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(--media-fg-color); color: var(--video-fg-color);
background-color: var(--media-bg-color); background-color: var(--video-bg-color);
box-shadow: inset 0 0 0 2px var(--media-outline-color); box-shadow: inset 0 0 0 2px var(--video-outline-color);
display: flex; display: flex;
place-content: center; place-content: center;
place-items: center; place-items: center;
@ -912,9 +893,9 @@ body:has(#modal-container .carousel) .status .media img:hover {
position: absolute; position: absolute;
bottom: 8px; bottom: 8px;
right: 8px; right: 8px;
color: var(--media-fg-color); color: var(--video-fg-color);
background-color: var(--media-bg-color); background-color: var(--video-bg-color);
border: var(--hairline-width) solid var(--media-outline-color); border: var(--hairline-width) solid var(--video-outline-color);
border-radius: 4px; border-radius: 4px;
padding: 0 4px; padding: 0 4px;
} }
@ -929,9 +910,9 @@ body:has(#modal-container .carousel) .status .media img:hover {
position: absolute; position: absolute;
bottom: 8px; bottom: 8px;
right: 8px; right: 8px;
color: var(--media-fg-color); color: var(--bg-faded-color);
background-color: var(--media-bg-color); background-color: var(--text-insignificant-color);
border: var(--hairline-width) solid var(--media-outline-color); backdrop-filter: blur(6px) saturate(3) invert(0.2);
border-radius: 4px; border-radius: 4px;
padding: 0 4px; padding: 0 4px;
} }
@ -998,62 +979,6 @@ 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;
} }
@ -1078,12 +1003,6 @@ 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;
@ -1719,37 +1638,3 @@ 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;
}
}
}

View file

@ -9,6 +9,7 @@ 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 {
@ -21,6 +22,7 @@ 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';
@ -41,7 +43,6 @@ 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';
@ -55,7 +56,6 @@ 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,9 +67,13 @@ const throttle = pThrottle({
}); });
function fetchAccount(id, masto) { function fetchAccount(id, masto) {
return masto.v1.accounts.$select(id).fetch(); try {
return masto.v1.accounts.fetch(id);
} catch (e) {
return Promise.reject(e);
}
} }
const memFetchAccount = pmem(fetchAccount); const memFetchAccount = mem(fetchAccount);
const visibilityText = { const visibilityText = {
public: 'Public', public: 'Public',
@ -386,11 +390,11 @@ function Status({
reblogsCount: reblogsCount + (reblogged ? -1 : 1), reblogsCount: reblogsCount + (reblogged ? -1 : 1),
}; };
if (reblogged) { if (reblogged) {
const newStatus = await masto.v1.statuses.$select(id).unreblog(); const newStatus = await masto.v1.statuses.unreblog(id);
saveStatus(newStatus, instance); saveStatus(newStatus, instance);
return true; return true;
} else { } else {
const newStatus = await masto.v1.statuses.$select(id).reblog(); const newStatus = await masto.v1.statuses.reblog(id);
saveStatus(newStatus, instance); saveStatus(newStatus, instance);
return true; return true;
} }
@ -414,11 +418,11 @@ function Status({
reblogsCount: reblogsCount + (reblogged ? -1 : 1), reblogsCount: reblogsCount + (reblogged ? -1 : 1),
}; };
if (reblogged) { if (reblogged) {
const newStatus = await masto.v1.statuses.$select(id).unreblog(); const newStatus = await masto.v1.statuses.unreblog(id);
saveStatus(newStatus, instance); saveStatus(newStatus, instance);
return true; return true;
} else { } else {
const newStatus = await masto.v1.statuses.$select(id).reblog(); const newStatus = await masto.v1.statuses.reblog(id);
saveStatus(newStatus, instance); saveStatus(newStatus, instance);
return true; return true;
} }
@ -442,10 +446,10 @@ function Status({
favouritesCount: favouritesCount + (favourited ? -1 : 1), favouritesCount: favouritesCount + (favourited ? -1 : 1),
}; };
if (favourited) { if (favourited) {
const newStatus = await masto.v1.statuses.$select(id).unfavourite(); const newStatus = await masto.v1.statuses.unfavourite(id);
saveStatus(newStatus, instance); saveStatus(newStatus, instance);
} else { } else {
const newStatus = await masto.v1.statuses.$select(id).favourite(); const newStatus = await masto.v1.statuses.favourite(id);
saveStatus(newStatus, instance); saveStatus(newStatus, instance);
} }
} catch (e) { } catch (e) {
@ -466,10 +470,10 @@ function Status({
bookmarked: !bookmarked, bookmarked: !bookmarked,
}; };
if (bookmarked) { if (bookmarked) {
const newStatus = await masto.v1.statuses.$select(id).unbookmark(); const newStatus = await masto.v1.statuses.unbookmark(id);
saveStatus(newStatus, instance); saveStatus(newStatus, instance);
} else { } else {
const newStatus = await masto.v1.statuses.$select(id).bookmark(); const newStatus = await masto.v1.statuses.bookmark(id);
saveStatus(newStatus, instance); saveStatus(newStatus, instance);
} }
} catch (e) { } catch (e) {
@ -480,7 +484,7 @@ function Status({
}; };
const differentLanguage = const differentLanguage =
!!language && language &&
language !== targetLanguage && language !== targetLanguage &&
!localeMatch([language], [targetLanguage]) && !localeMatch([language], [targetLanguage]) &&
!contentTranslationHideLanguages.find( !contentTranslationHideLanguages.find(
@ -704,9 +708,9 @@ function Status({
<MenuItem <MenuItem
onClick={async () => { onClick={async () => {
try { try {
const newStatus = await masto.v1.statuses const newStatus = await masto.v1.statuses[
.$select(id) muted ? 'unmute' : 'mute'
[muted ? 'unmute' : 'mute'](); ](id);
saveStatus(newStatus, instance); saveStatus(newStatus, instance);
showToast(muted ? 'Conversation unmuted' : 'Conversation muted'); showToast(muted ? 'Conversation unmuted' : 'Conversation muted');
} catch (e) { } catch (e) {
@ -759,7 +763,7 @@ function Status({
// if (yes) { // if (yes) {
(async () => { (async () => {
try { try {
await masto.v1.statuses.$select(id).remove(); await masto.v1.statuses.remove(id);
const cachedStatus = getStatus(id, instance); const cachedStatus = getStatus(id, instance);
cachedStatus._deleted = true; cachedStatus._deleted = true;
showToast('Deleted'); showToast('Deleted');
@ -786,17 +790,8 @@ 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(
isIOS (e) => {
? (e) => {
if (e.pointerType === 'mouse') return;
// There's 'pen' too, but not sure if contextmenu event would trigger from a pen
const { clientX, clientY } = e.touches?.[0] || e; const { clientX, clientY } = e.touches?.[0] || e;
// link detection copied from onContextMenu because here it works // link detection copied from onContextMenu because here it works
const link = e.target.closest('a'); const link = e.target.closest('a');
@ -807,13 +802,12 @@ function Status({
y: clientY, y: clientY,
}); });
setIsContextMenuOpen(true); setIsContextMenuOpen(true);
} },
: null,
{ {
threshold: 600, threshold: 600,
captureEvent: true, captureEvent: true,
detect: 'touch', detect: 'touch',
cancelOnMovement: 2, // true allows movement of up to 25 pixels cancelOnMovement: 4, // true allows movement of up to 25 pixels
}, },
); );
@ -868,72 +862,6 @@ 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) => {
@ -1040,14 +968,13 @@ function Status({
)} )}
<div class="container"> <div class="container">
<div class="meta"> <div class="meta">
<span class="meta-name"> {/* <span> */}
<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' && (
<> <>
{' '} {' '}
@ -1264,8 +1191,7 @@ function Status({
}} }}
refresh={() => { refresh={() => {
return masto.v1.polls return masto.v1.polls
.$select(poll.id) .fetch(poll.id)
.fetch()
.then((pollResponse) => { .then((pollResponse) => {
states.statuses[sKey].poll = pollResponse; states.statuses[sKey].poll = pollResponse;
}) })
@ -1273,8 +1199,7 @@ function Status({
}} }}
votePoll={(choices) => { votePoll={(choices) => {
return masto.v1.polls return masto.v1.polls
.$select(poll.id) .vote(poll.id, {
.votes.create({
choices, choices,
}) })
.then((pollResponse) => { .then((pollResponse) => {
@ -1330,27 +1255,19 @@ function Status({
</button> </button>
)} )}
{!!mediaAttachments.length && ( {!!mediaAttachments.length && (
<MultipleMediaFigure
lang={language}
enabled={showMultipleMediaCaptions}
captionChildren={captionChildren}
>
<div <div
ref={mediaContainerRef} ref={mediaContainerRef}
class={`media-container media-eq${mediaAttachments.length} ${ class={`media-container media-eq${mediaAttachments.length} ${
mediaAttachments.length > 2 ? 'media-gt2' : '' mediaAttachments.length > 2 ? 'media-gt2' : ''
} ${mediaAttachments.length > 4 ? 'media-gt4' : ''}`} } ${mediaAttachments.length > 4 ? 'media-gt4' : ''}`}
> >
{displayedMediaAttachments.map((media, i) => ( {mediaAttachments
.slice(0, isSizeLarge ? undefined : 4)
.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}`}
@ -1364,7 +1281,6 @@ function Status({
/> />
))} ))}
</div> </div>
</MultipleMediaFigure>
)} )}
{!!card && {!!card &&
card?.url !== status.url && card?.url !== status.url &&
@ -1532,7 +1448,7 @@ function Status({
statusID={showEdited} statusID={showEdited}
instance={instance} instance={instance}
fetchStatusHistory={() => { fetchStatusHistory={() => {
return masto.v1.statuses.$select(showEdited).history.list(); return masto.v1.statuses.listHistory(showEdited);
}} }}
onClose={() => { onClose={() => {
setShowEdited(false); setShowEdited(false);
@ -1561,19 +1477,6 @@ 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 {
@ -1582,18 +1485,14 @@ 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
@ -1619,7 +1518,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.$select(id).fetch(); // const status = await masto.v1.statuses.fetch(id);
// saveStatus(status, instance); // saveStatus(status, instance);
// setCardStatusID(id); // setCardStatusID(id);
// })(); // })();
@ -1666,7 +1565,7 @@ function Card({ card, instance }) {
width={width} width={width}
height={height} height={height}
loading="lazy" loading="lazy"
alt={imageDescription || ''} alt=""
onError={(e) => { onError={(e) => {
try { try {
e.target.style.display = 'none'; e.target.style.display = 'none';
@ -1839,16 +1738,15 @@ function ReactionsModal({ statusID, instance, onClose }) {
(async () => { (async () => {
try { try {
if (firstLoad) { if (firstLoad) {
reblogIterator.current = masto.v1.statuses reblogIterator.current = masto.v1.statuses.listRebloggedBy(statusID, {
.$select(statusID)
.rebloggedBy.list({
limit: REACTIONS_LIMIT, limit: REACTIONS_LIMIT,
}); });
favouriteIterator.current = masto.v1.statuses favouriteIterator.current = masto.v1.statuses.listFavouritedBy(
.$select(statusID) statusID,
.favouritedBy.list({ {
limit: REACTIONS_LIMIT, limit: REACTIONS_LIMIT,
}); },
);
} }
const [{ value: reblogResults }, { value: favouriteResults }] = const [{ value: reblogResults }, { value: favouriteResults }] =
await Promise.allSettled([ await Promise.allSettled([
@ -2078,10 +1976,7 @@ 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 remoteInstanceFetch = masto.v1.statuses.fetch(id).then((status) => {
.$select(id)
.fetch()
.then((status) => {
if (status?.id) { if (status?.id) {
return { return {
status, status,
@ -2094,8 +1989,8 @@ function _unfurlMastodonLink(instance, url) {
} }
const { masto } = api({ instance }); const { masto } = api({ instance });
const mastoSearchFetch = masto.v2.search const mastoSearchFetch = masto.v2
.fetch({ .search({
q: url, q: url,
type: 'statuses', type: 'statuses',
resolve: true, resolve: true,
@ -2165,7 +2060,11 @@ function nicePostURL(url) {
); );
} }
const unfurlMastodonLink = throttle(_unfurlMastodonLink); const unfurlMastodonLink = throttle(
mem(_unfurlMastodonLink, {
cacheKey: (instance, url) => `${instance}:${url}`,
}),
);
function FilteredStatus({ status, filterInfo, instance, containerProps = {} }) { function FilteredStatus({ status, filterInfo, instance, containerProps = {} }) {
const { const {
@ -2188,7 +2087,7 @@ function FilteredStatus({ status, filterInfo, instance, containerProps = {} }) {
threshold: 600, threshold: 600,
captureEvent: true, captureEvent: true,
detect: 'touch', detect: 'touch',
cancelOnMovement: 2, // true allows movement of up to 25 pixels cancelOnMovement: 4, // true allows movement of up to 25 pixels
}, },
); );

View file

@ -14,15 +14,10 @@ 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,
@ -117,7 +112,7 @@ function Timeline({
} }
if (nextItem) { if (nextItem) {
nextItem.focus(); nextItem.focus();
nextItem.scrollIntoView(scrollIntoViewOptions); nextItem.scrollIntoViewIfNeeded?.();
} }
} 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
@ -127,7 +122,7 @@ function Timeline({
}); });
if (topmostItem) { if (topmostItem) {
topmostItem.focus(); topmostItem.focus();
topmostItem.scrollIntoView(scrollIntoViewOptions); topmostItem.scrollIntoViewIfNeeded?.();
} }
} }
}); });
@ -156,7 +151,7 @@ function Timeline({
} }
if (prevItem) { if (prevItem) {
prevItem.focus(); prevItem.focus();
prevItem.scrollIntoView(scrollIntoViewOptions); prevItem.scrollIntoViewIfNeeded?.();
} }
} 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
@ -166,7 +161,7 @@ function Timeline({
}); });
if (topmostItem) { if (topmostItem) {
topmostItem.focus(); topmostItem.focus();
topmostItem.scrollIntoView(scrollIntoViewOptions); topmostItem.scrollIntoViewIfNeeded?.();
} }
} }
}); });
@ -418,7 +413,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 =
(!_differentAuthor && isSpoiler && i > 0) || (isSpoiler && i > 0) ||
(manyItems && (manyItems &&
isMiddle && isMiddle &&
(type === 'thread' || (type === 'thread' ||

View file

@ -7,7 +7,6 @@ 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';
@ -26,7 +25,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];
@ -56,18 +55,11 @@ function _lingvaTranslate(text, source, target) {
); );
}, },
}); });
// return masto.v1.statuses.$select(id).translate({ // return masto.v1.statuses.translate(id, {
// lang: DEFAULT_LANG, // lang: DEFAULT_LANG,
// }); // });
} }
const TRANSLATED_MAX_AGE = 1000 * 60 * 60; // 1 hour const throttledLingvaTranslate = throttle(lingvaTranslate);
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,

View file

@ -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: #ffa60020; --reply-to-faded-color: #ffa60030;
--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 */
--media-fg-color: #f0f2f5; --video-fg-color: #f0f2f5;
--media-bg-color: #242526; --video-bg-color: #242526;
--media-outline-color: color-mix(in lch, var(--media-fg-color), transparent); --video-outline-color: color-mix(in lch, var(--video-fg-color), transparent);
--timing-function: cubic-bezier(0.3, 0.5, 0, 1); --timing-function: cubic-bezier(0.3, 0.5, 0, 1);
} }
@ -92,14 +92,9 @@
--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: #ffa60017; --reply-to-faded-color: #ffa60027;
--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);

View file

@ -1,4 +1,4 @@
import { MenuItem } from '@szhsin/react-menu'; import { Menu, 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,8 +31,7 @@ function AccountStatuses() {
const results = []; const results = [];
if (firstLoad) { if (firstLoad) {
const { value: pinnedStatuses } = await masto.v1.accounts const { value: pinnedStatuses } = await masto.v1.accounts
.$select(id) .listStatuses(id, {
.statuses.list({
pinned: true, pinned: true,
}) })
.next(); .next();
@ -54,9 +53,7 @@ function AccountStatuses() {
} }
} }
if (firstLoad || !accountStatusesIterator.current) { if (firstLoad || !accountStatusesIterator.current) {
accountStatusesIterator.current = masto.v1.accounts accountStatusesIterator.current = masto.v1.accounts.listStatuses(id, {
.$select(id)
.statuses.list({
limit: LIMIT, limit: LIMIT,
exclude_replies: excludeReplies, exclude_replies: excludeReplies,
exclude_reblogs: excludeBoosts, exclude_reblogs: excludeBoosts,
@ -89,16 +86,14 @@ function AccountStatuses() {
useEffect(() => { useEffect(() => {
(async () => { (async () => {
try { try {
const acc = await masto.v1.accounts.$select(id).fetch(); const acc = await masto.v1.accounts.fetch(id);
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 const featuredTags = await masto.v1.accounts.listFeaturedTags(id);
.$select(id)
.featuredTags.list(id);
console.log({ featuredTags }); console.log({ featuredTags });
setFeaturedTags(featuredTags); setFeaturedTags(featuredTags);
} catch (e) { } catch (e) {
@ -118,7 +113,7 @@ function AccountStatuses() {
<AccountInfo <AccountInfo
instance={instance} instance={instance}
account={cachedAccount || id} account={cachedAccount || id}
fetchAccount={() => masto.v1.accounts.$select(id).fetch()} fetchAccount={() => masto.v1.accounts.fetch(id)}
authenticated={authenticated} authenticated={authenticated}
standalone standalone
/> />

View file

@ -52,9 +52,9 @@ function Accounts({ onClose }) {
onDblClick={async () => { onDblClick={async () => {
if (isCurrent) { if (isCurrent) {
try { try {
const info = await masto.v1.accounts const info = await masto.v1.accounts.fetch(
.$select(account.info.id) 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);

View file

@ -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, streaming, instance } = api(); const { masto, 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.home.list({ limit: LIMIT }); homeIterator.current = masto.v1.timelines.listHome({ 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.home const results = await masto.v1.timelines
.list({ .listHome({
limit: 5, limit: 5,
since_id: latestItem.current, since_id: latestItem.current,
}) })
@ -75,33 +75,52 @@ function Following({ title, path, id, ...props }) {
} }
} }
useEffect(() => { const ws = useRef();
let sub; const streamUser = async () => {
(async () => { console.log('🎏 Start streaming user', ws.current);
if (streaming) { if (
sub = streaming.user.subscribe(); ws.current &&
console.log('🎏 Streaming user', sub); (ws.current.readyState === WebSocket.CONNECTING ||
for await (const entry of sub) { ws.current.readyState === WebSocket.OPEN)
if (!sub) break; ) {
if (entry.event === 'status.update') { console.log('🎏 Streaming user already open');
const status = entry.payload; 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`); console.log(`🔄 Status ${status.id} updated`);
saveStatus(status, instance); saveStatus(status, instance);
} else if (entry.event === 'delete') { });
const statusID = entry.payload;
stream.on('delete', (statusID) => {
console.log(`❌ Status ${statusID} deleted`); console.log(`❌ Status ${statusID} deleted`);
// delete states.statuses[statusID]; // delete states.statuses[statusID];
const s = getStatus(statusID, instance); const s = getStatus(statusID, instance);
if (s) s._deleted = true; if (s) s._deleted = true;
} });
}
} stream.ws.onclose = () => {
console.log('🎏 Streaming user closed');
};
return stream;
};
useEffect(() => {
let stream;
(async () => {
stream = await streamUser();
})(); })();
return () => { return () => {
sub?.unsubscribe?.(); if (stream) {
sub = null; stream.ws.close();
ws.current = null;
}
}; };
}, [streaming]); }, []);
return ( return (
<Timeline <Timeline

View file

@ -1,5 +1,6 @@
import { import {
FocusableItem, FocusableItem,
Menu,
MenuDivider, MenuDivider,
MenuGroup, MenuGroup,
MenuItem, MenuItem,
@ -46,7 +47,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.tag.$select(hashtag).list({ // hashtagsIterator.current = masto.v1.timelines.listHashtag(hashtag, {
// limit: LIMIT, // limit: LIMIT,
// any: hashtags.slice(1), // any: hashtags.slice(1),
// }); // });
@ -54,9 +55,8 @@ 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.tag const results = await masto.v1.timelines
.$select(hashtag) .listHashtag(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,9 +82,8 @@ function Hashtags({ columnMode, ...props }) {
async function checkForUpdates() { async function checkForUpdates() {
try { try {
const results = await masto.v1.timelines.tag const results = await masto.v1.timelines
.$select(hashtag) .listHashtag(hashtag, {
.list({
limit: 1, limit: 1,
any: hashtags.slice(1), any: hashtags.slice(1),
since_id: latestItem.current, since_id: latestItem.current,
@ -106,7 +105,7 @@ function Hashtags({ columnMode, ...props }) {
useEffect(() => { useEffect(() => {
(async () => { (async () => {
try { try {
const info = await masto.v1.tags.$select(hashtag).fetch(); const info = await masto.v1.tags.fetch(hashtag);
console.log(info); console.log(info);
setInfo(info); setInfo(info);
} catch (e) { } catch (e) {
@ -165,8 +164,7 @@ function Hashtags({ columnMode, ...props }) {
// return; // return;
// } // }
masto.v1.tags masto.v1.tags
.$select(hashtag) .unfollow(hashtag)
.unfollow()
.then(() => { .then(() => {
setInfo({ ...info, following: false }); setInfo({ ...info, following: false });
showToast(`Unfollowed #${hashtag}`); showToast(`Unfollowed #${hashtag}`);
@ -180,8 +178,7 @@ function Hashtags({ columnMode, ...props }) {
}); });
} else { } else {
masto.v1.tags masto.v1.tags
.$select(hashtag) .follow(hashtag)
.follow()
.then(() => { .then(() => {
setInfo({ ...info, following: true }); setInfo({ ...info, following: true });
showToast(`Followed #${hashtag}`); showToast(`Followed #${hashtag}`);
@ -261,14 +258,11 @@ 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('+')}`
// : `/t/${hashtags.join('+')}`,
// );
location.hash = instance
? `/${instance}/t/${hashtags.join('+')}` ? `/${instance}/t/${hashtags.join('+')}`
: `/t/${hashtags.join('+')}`; : `/t/${hashtags.join('+')}`,
);
}} }}
> >
<Icon icon="x" alt="Remove hashtag" class="danger-icon" /> <Icon icon="x" alt="Remove hashtag" class="danger-icon" />
@ -323,8 +317,7 @@ 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('+')}`;
} }
}} }}
> >

View file

@ -13,6 +13,7 @@ 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';
@ -48,6 +49,24 @@ 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> */}
</> </>
); );
} }

View file

@ -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.list.$select(id).list({ listIterator.current = masto.v1.timelines.listList(id, {
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.list.$select(id).list({ const results = await masto.v1.timelines.listList(id, {
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.$select(id).fetch(); const list = await masto.v1.lists.fetch(id);
setList(list); setList(list);
// setTitle(list.title); // setTitle(list.title);
} catch (e) { } catch (e) {
@ -200,9 +200,7 @@ function ListManageMembers({ listID, onClose }) {
(async () => { (async () => {
try { try {
if (firstLoad || !membersIterator.current) { if (firstLoad || !membersIterator.current) {
membersIterator.current = masto.v1.lists membersIterator.current = masto.v1.lists.listAccounts(listID, {
.$select(listID)
.accounts.list({
limit: MEMBERS_LIMIT, limit: MEMBERS_LIMIT,
}); });
} }
@ -276,7 +274,7 @@ function RemoveAddButton({ account, listID }) {
setUIState('loading'); setUIState('loading');
(async () => { (async () => {
try { try {
await masto.v1.lists.$select(listID).accounts.create({ await masto.v1.lists.addAccount(listID, {
accountIds: [account.id], accountIds: [account.id],
}); });
setUIState('default'); setUIState('default');
@ -292,7 +290,7 @@ function RemoveAddButton({ account, listID }) {
(async () => { (async () => {
try { try {
await masto.v1.lists.$select(listID).accounts.remove({ await masto.v1.lists.removeAccount(listID, {
accountIds: [account.id], accountIds: [account.id],
}); });
setUIState('default'); setUIState('default');

View file

@ -92,16 +92,11 @@ function Notifications({ columnMode }) {
return allNotifications; return allNotifications;
} }
async function fetchFollowRequests() { 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???
try { return masto.v1.followRequests.list({
return await masto.v1.followRequests.list({
limit: 80, limit: 80,
}); });
} catch (e) {
// Silently fail
return [];
}
} }
const loadFollowRequests = () => { const loadFollowRequests = () => {
@ -117,13 +112,8 @@ function Notifications({ columnMode }) {
})(); })();
}; };
async function fetchAnnouncements() { function fetchAnnouncements() {
try { return masto.v1.announcements.list();
return await masto.v1.announcements.list();
} catch (e) {
// Silently fail
return [];
}
} }
const loadNotifications = (firstLoad) => { const loadNotifications = (firstLoad) => {
@ -389,10 +379,7 @@ function Notifications({ columnMode }) {
)} )}
{snapStates.notifications.length ? ( {snapStates.notifications.length ? (
<> <>
{snapStates.notifications {snapStates.notifications.map((notification) => {
// This is leaked from Notifications popover
.filter((n) => n.type !== 'follow_request')
.map((notification) => {
if (onlyMentions && notification.type !== 'mention') { if (onlyMentions && notification.type !== 'mention') {
return null; return null;
} }
@ -405,8 +392,7 @@ function Notifications({ columnMode }) {
// if notificationDay is yesterday, show "Yesterday" // if notificationDay is yesterday, show "Yesterday"
// if notificationDay is before yesterday, show date // if notificationDay is before yesterday, show date
const heading = const heading =
notificationDay.toDateString() === notificationDay.toDateString() === yesterdayDate.toDateString()
yesterdayDate.toDateString()
? 'Yesterday' ? 'Yesterday'
: niceDateTime(currentDay, { : niceDateTime(currentDay, {
hideTime: true, hideTime: true,

View file

@ -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.public.list({ publicIterator.current = masto.v1.timelines.listPublic({
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.public const results = await masto.v1.timelines
.list({ .listPublic({
limit: 1, limit: 1,
local: isLocal, local: isLocal,
since_id: latestItem.current, since_id: latestItem.current,

View file

@ -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.fetch(params); const results = await masto.v2.search(params);
console.log(results); console.log(results);
if (type) { if (type) {
if (firstLoad) { if (firstLoad) {

View file

@ -135,8 +135,3 @@
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;
}

View file

@ -4,7 +4,6 @@ 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';
@ -35,7 +34,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, instance } = api(); const { masto, authenticated } = 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(() => {
@ -179,8 +178,7 @@ 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>
@ -219,19 +217,6 @@ 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>
@ -354,7 +339,6 @@ 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>
@ -451,7 +435,6 @@ 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';
@ -475,7 +458,6 @@ 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';
@ -486,26 +468,9 @@ function Settings({ onClose }) {
</div> </div>
</div> </div>
<p> <p>
<a
href="https://github.com/sponsors/cheeaun"
target="_blank"
rel="noopener noreferrer"
>
Sponsor
</a>{' '}
&middot;{' '}
<a
href="https://www.buymeacoffee.com/cheeaun"
target="_blank"
rel="noopener noreferrer"
>
Donate
</a>{' '}
&middot;{' '}
<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>

View file

@ -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, useSearchParams } from 'react-router-dom'; import { matchPath, useParams, useSearchParams } from 'react-router-dom';
import { useDebouncedCallback } from 'use-debounce'; import { useDebouncedCallback } from 'use-debounce';
import { useSnapshot } from 'valtio'; import { useSnapshot } from 'valtio';
@ -54,12 +54,6 @@ 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 });
@ -100,7 +94,7 @@ function StatusPage(params) {
if (!heroStatus && showMedia) { if (!heroStatus && showMedia) {
(async () => { (async () => {
try { try {
const status = await masto.v1.statuses.$select(id).fetch(); const status = await masto.v1.statuses.fetch(id);
saveStatus(status, instance); saveStatus(status, instance);
setHeroStatus(status); setHeroStatus(status);
} catch (err) { } catch (err) {
@ -141,7 +135,6 @@ 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}
/> />
@ -235,15 +228,12 @@ function StatusThread({ id, closeLink = '/', instance: propInstance }) {
(async () => { (async () => {
const heroFetch = () => const heroFetch = () =>
pRetry(() => masto.v1.statuses.$select(id).fetch(), { pRetry(() => masto.v1.statuses.fetch(id), {
retries: 4, retries: 4,
}); });
const contextFetch = pRetry( const contextFetch = pRetry(() => masto.v1.statuses.fetchContext(id), {
() => masto.v1.statuses.$select(id).context.fetch(),
{
retries: 8, retries: 8,
}, });
);
const hasStatus = !!snapStates.statuses[sKey]; const hasStatus = !!snapStates.statuses[sKey];
let heroStatus = snapStates.statuses[sKey]; let heroStatus = snapStates.statuses[sKey];
@ -564,7 +554,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.scrollIntoView(scrollIntoViewOptions); nextStatus.scrollIntoViewIfNeeded?.();
} }
} 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
@ -574,7 +564,7 @@ function StatusThread({ id, closeLink = '/', instance: propInstance }) {
}); });
if (topmostStatusLink) { if (topmostStatusLink) {
topmostStatusLink.focus(); topmostStatusLink.focus();
topmostStatusLink.scrollIntoView(scrollIntoViewOptions); topmostStatusLink.scrollIntoViewIfNeeded?.();
} }
} }
}); });
@ -598,7 +588,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.scrollIntoView(scrollIntoViewOptions); prevStatus.scrollIntoViewIfNeeded?.();
} }
} 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
@ -608,7 +598,7 @@ function StatusThread({ id, closeLink = '/', instance: propInstance }) {
}); });
if (topmostStatusLink) { if (topmostStatusLink) {
topmostStatusLink.focus(); topmostStatusLink.focus();
topmostStatusLink.scrollIntoView(scrollIntoViewOptions); topmostStatusLink.scrollIntoViewIfNeeded?.();
} }
} }
}); });
@ -949,8 +939,7 @@ function StatusThread({ id, closeLink = '/', instance: propInstance }) {
setUIState('loading'); setUIState('loading');
(async () => { (async () => {
try { try {
const results = const results = await currentMasto.v2.search({
await currentMasto.v2.search.fetch({
q: heroStatus.url, q: heroStatus.url,
type: 'statuses', type: 'statuses',
resolve: true, resolve: true,

View file

@ -1,4 +1,4 @@
import { MenuItem } from '@szhsin/react-menu'; import { Menu, 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.statuses.list({ trendIterator.current = masto.v1.trends.listStatuses({
limit: LIMIT, limit: LIMIT,
}); });
// Get hashtags // Get hashtags
try { try {
const iterator = masto.v1.trends.tags.list(); const iterator = masto.v1.trends.listTags();
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.statuses const results = await masto.v1.trends
.list({ .listStatuses({
limit: 1, limit: 1,
// NOT SUPPORTED // NOT SUPPORTED
// since_id: latestItem.current, // since_id: latestItem.current,

View file

@ -7,7 +7,6 @@ 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';

View file

@ -1,4 +1,4 @@
import { createRestAPIClient, createStreamingAPIClient } from 'masto'; import { createClient } from 'masto';
import store from './store'; import store from './store';
import { import {
@ -37,17 +37,14 @@ export function initClient({ instance, accessToken }) {
} }
const url = instance ? `https://${instance}` : `https://${DEFAULT_INSTANCE}`; const url = instance ? `https://${instance}` : `https://${DEFAULT_INSTANCE}`;
const masto = createRestAPIClient({ const client = createClient({
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;
@ -58,8 +55,7 @@ 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) {
console.log('INIT INSTANCE', client, instance); const masto = client;
const { masto, accessToken } = client;
// Request v2, fallback to v1 if fail // Request v2, fallback to v1 if fail
let info; let info;
try { try {
@ -67,7 +63,7 @@ export async function initInstance(client, instance) {
} catch (e) {} } catch (e) {}
if (!info) { if (!info) {
try { try {
info = await masto.v1.instance.fetch(); info = await masto.v1.instances.fetch();
} catch (e) {} } catch (e) {}
} }
if (!info) return; if (!info) return;
@ -95,28 +91,17 @@ 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
const supportsWebSocket = 'WebSocket' in window; if (streamingApi || streaming) {
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({
@ -130,7 +115,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) {
@ -149,14 +134,10 @@ 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:
streaming, accountApis[instance]?.[accessToken] ||
client, initClient({ instance, accessToken }),
authenticated: true, authenticated: true,
instance, instance,
}; };
@ -168,12 +149,8 @@ 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, masto: accountApis[instance][accessToken],
streaming,
client,
authenticated: true, authenticated: true,
instance, instance,
}; };
@ -183,17 +160,13 @@ 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, masto: initClient({ instance, accessToken }),
streaming,
client,
authenticated: true, authenticated: true,
instance, instance,
}; };
} else { } else {
throw new Error(`Access token not found`); throw new Error(`Access token ${accessToken} not found`);
} }
} }
} }
@ -205,14 +178,10 @@ 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:
streaming, accountApis[instance]?.[accessToken] ||
client, initClient({ instance, accessToken }),
authenticated: true, authenticated: true,
instance, instance,
}; };
@ -223,13 +192,10 @@ 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 client = apis[instance] || initClient({ instance }); const masto = apis[instance] || initClient({ instance });
const { masto, streaming, accessToken } = client;
return { return {
masto, masto,
streaming, authenticated: !!masto.config.props.accessToken,
client,
authenticated: !!accessToken,
instance, instance,
}; };
} }
@ -237,11 +203,9 @@ 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, masto: currentAccountApi,
streaming: currentAccountApi.streaming,
client: currentAccountApi,
authenticated: true, authenticated: true,
instance: currentAccountApi.instance, instance: currentAccountApi.__instance__,
}; };
} }
const currentAccount = getCurrentAccount(); const currentAccount = getCurrentAccount();
@ -251,22 +215,15 @@ export function api({ instance, accessToken, accountID, account } = {}) {
accountApis[instance]?.[accessToken] || accountApis[instance]?.[accessToken] ||
initClient({ instance, accessToken }); initClient({ instance, accessToken });
return { return {
masto: currentAccountApi.masto, masto: currentAccountApi,
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, masto: apis[DEFAULT_INSTANCE] || initClient({ instance: DEFAULT_INSTANCE }),
streaming,
client,
authenticated: false, authenticated: false,
instance: DEFAULT_INSTANCE, instance: DEFAULT_INSTANCE,
}; };

View file

@ -1,10 +1,9 @@
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');
@ -251,7 +250,6 @@ function _enhanceContent(content, opts = {}) {
return enhancedContent; return enhancedContent;
} }
const enhanceContent = mem(_enhanceContent);
const defaultRejectFilter = [ const defaultRejectFilter = [
// Document metadata // Document metadata

View file

@ -3,11 +3,6 @@ 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;
@ -16,7 +11,10 @@ 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',
); );

View file

@ -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|posts)\/\w+\/?$/i.test(pathname) || // GoToSocial, Takahe /^\/@[^/]+\/statuses\/\w+$/i.test(pathname) || // GoToSocial
/^\/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
); );

View file

@ -1,6 +1,5 @@
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
@ -11,6 +10,8 @@ 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;

View file

@ -1,5 +0,0 @@
import moize from 'moize';
export default function mem(fn, opts = {}) {
return moize(fn, { ...opts, maxSize: 100 });
}

View file

@ -1,5 +0,0 @@
import mem from './mem';
export default function pmem(fn, opts = {}) {
return mem(fn, { isPromise: true, ...opts });
}

View file

@ -34,22 +34,22 @@ import { getCurrentAccount } from './store-utils';
function createBackendPushSubscription(subscription) { function createBackendPushSubscription(subscription) {
const { masto } = api(); const { masto } = api();
return masto.v1.push.subscription.create(subscription); return masto.v1.webPushSubscriptions.create(subscription);
} }
function fetchBackendPushSubscription() { function fetchBackendPushSubscription() {
const { masto } = api(); const { masto } = api();
return masto.v1.push.subscription.fetch(); return masto.v1.webPushSubscriptions.fetch();
} }
function updateBackendPushSubscription(subscription) { function updateBackendPushSubscription(subscription) {
const { masto } = api(); const { masto } = api();
return masto.v1.push.subscription.update(subscription); return masto.v1.webPushSubscriptions.update(subscription);
} }
function removeBackendPushSubscription() { function removeBackendPushSubscription() {
const { masto } = api(); const { masto } = api();
return masto.v1.push.subscription.remove(); return masto.v1.webPushSubscriptions.remove();
} }
// Front-end // Front-end

View file

@ -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,16 +18,12 @@ const states = proxy({
homeLast: null, // Last item in 'home' list homeLast: null, // Last item in 'home' list
homeLastFetchTime: null, homeLastFetchTime: null,
notifications: [], notifications: [],
notificationsLast: null, // Last read notification notificationsLast: store.account.get('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: {},
@ -43,21 +39,24 @@ const states = proxy({
showMediaModal: false, showMediaModal: false,
showShortcutsSettings: false, showShortcutsSettings: false,
showKeyboardShortcutsHelp: false, showKeyboardShortcutsHelp: false,
showGenericAccounts: false,
showMediaAlt: false,
// Shortcuts // Shortcuts
shortcuts: [], shortcuts: store.account.get('shortcuts') ?? [],
// Settings // Settings
settings: { settings: {
autoRefresh: false, autoRefresh: store.account.get('settings-autoRefresh') ?? false,
shortcutsViewMode: null, shortcutsViewMode: store.account.get('settings-shortcutsViewMode') ?? null,
shortcutsColumnsMode: false, shortcutsColumnsMode:
boostsCarousel: true, store.account.get('settings-shortcutsColumnsMode') ?? false,
contentTranslation: true, boostsCarousel: store.account.get('settings-boostsCarousel') ?? true,
contentTranslationTargetLanguage: null, contentTranslation:
contentTranslationHideLanguages: [], store.account.get('settings-contentTranslation') ?? true,
contentTranslationAutoInline: false, contentTranslationTargetLanguage:
cloakMode: false, store.account.get('settings-contentTranslationTargetLanguage') || null,
contentTranslationHideLanguages:
store.account.get('settings-contentTranslationHideLanguages') || [],
contentTranslationAutoInline:
store.account.get('settings-contentTranslationAutoInline') ?? false,
cloakMode: store.account.get('settings-cloakMode') ?? false,
}, },
}); });
@ -141,7 +140,6 @@ 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) {
@ -207,7 +205,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.$.select(inReplyToId).fetch(); // prevStatus = await masto.v1.statuses.fetch(inReplyToId);
prevStatus = await fetchStatus(inReplyToId, masto); prevStatus = await fetchStatus(inReplyToId, masto);
saveStatus(prevStatus, instance, { skipThreading: true }); saveStatus(prevStatus, instance, { skipThreading: true });
} }
@ -229,6 +227,6 @@ export function threadifyStatus(status, propInstance) {
}); });
} }
const fetchStatus = pmem((statusID, masto) => { const fetchStatus = mem((statusID, masto) => {
return masto.v1.statuses.$select(statusID).fetch(); return masto.v1.statuses.fetch(statusID);
}); });

View file

@ -11,11 +11,6 @@ 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;

View file

@ -1,25 +1,17 @@
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, onResize: ({ height }) => {
if (ref.current) {
const { scrollHeight } = ref.current;
ref.current.classList.toggle(className, scrollHeight > height);
}
},
}); });
return ref; return ref;
} }