1
0
Fork 0

Compare commits

..

No commits in common. "1ed1e4222543d0c38e0b67c176f411603c5a317d" and "2e3bac2e1e670f05a39a71fcd905488955ad50c2" have entirely different histories.

29 changed files with 386 additions and 600 deletions

View file

@ -179,9 +179,6 @@ Available variables:
- May specify a self-hosted Lingva instance, powered by either [lingva-translate](https://github.com/thedaviddelta/lingva-translate) or [lingva-api](https://github.com/cheeaun/lingva-api) - May specify a self-hosted Lingva instance, powered by either [lingva-translate](https://github.com/thedaviddelta/lingva-translate) or [lingva-api](https://github.com/cheeaun/lingva-api)
- List of fallback instances hard-coded in `/.env` - List of fallback instances hard-coded in `/.env`
- [↗️ List of lingva-translate instances](https://github.com/thedaviddelta/lingva-translate?tab=readme-ov-file#instances) - [↗️ List of lingva-translate instances](https://github.com/thedaviddelta/lingva-translate?tab=readme-ov-file#instances)
- `PHANPY_IMG_ALT_API_URL` (optional, no defaults):
- API endpoint for self-hosted instance of [img-alt-api](https://github.com/cheeaun/img-alt-api).
- If provided, a setting will appear for users to enable the image description generator in the composer. Disabled by default.
- `PHANPY_GIPHY_API_KEY` (optional, no defaults): - `PHANPY_GIPHY_API_KEY` (optional, no defaults):
- API key for [GIPHY](https://developers.giphy.com/). See [API docs](https://developers.giphy.com/docs/api/). - API key for [GIPHY](https://developers.giphy.com/). See [API docs](https://developers.giphy.com/docs/api/).
- If provided, a setting will appear for users to enable the GIF picker in the composer. Disabled by default. - If provided, a setting will appear for users to enable the GIF picker in the composer. Disabled by default.
@ -208,7 +205,6 @@ These are self-hosted by other wonderful folks.
- [phanpy.hear-me.social](https://phanpy.hear-me.social) by [@admin@hear-me.social](https://hear-me.social/@admin) - [phanpy.hear-me.social](https://phanpy.hear-me.social) by [@admin@hear-me.social](https://hear-me.social/@admin)
- [phanpy.fulda.social](https://phanpy.fulda.social) by [@Ganneff@fulda.social](https://fulda.social/@Ganneff) - [phanpy.fulda.social](https://phanpy.fulda.social) by [@Ganneff@fulda.social](https://fulda.social/@Ganneff)
- [phanpy.crmbl.uk](https://phanpy.crmbl.uk) by [@snail@crmbl.uk](https://mstdn.crmbl.uk/@snail) - [phanpy.crmbl.uk](https://phanpy.crmbl.uk) by [@snail@crmbl.uk](https://mstdn.crmbl.uk/@snail)
- [halo.mookiesplace.com](https://halo.mookiesplace.com) by [@mookie@mookiesplace.com](https://mookiesplace.com/@mookie)
> Note: Add yours by creating a pull request. > Note: Add yours by creating a pull request.

View file

@ -306,20 +306,13 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
.timeline { .timeline {
> li:not(.timeline-item-carousel, .timeline-item-container) { > li:not(.timeline-item-carousel, .timeline-item-container) {
&:has(.status-media-first) { &:has(.status-media-first) {
@media (min-width: 40em) { width: fit-content;
width: fit-content;
max-width: min(480px, 100%);
}
background-color: transparent !important; background-color: transparent !important;
border: 0 !important; border: 0 !important;
box-shadow: none !important; box-shadow: none !important;
max-width: min(480px, 100%);
margin-inline: auto !important; margin-inline: auto !important;
&:not(:first-child) {
margin-block: 32px;
}
&:has(.skeleton) { &:has(.skeleton) {
width: 100%; width: 100%;
} }
@ -1918,8 +1911,7 @@ body > .szh-menu-container {
/* two columns only */ /* two columns only */
grid-template-columns: repeat(2, 1fr); grid-template-columns: repeat(2, 1fr);
} }
.szh-menu .menu-horizontal:has(> .szh-menu__item:only-child), .szh-menu .menu-horizontal:has(> .szh-menu__item:only-child) {
.szh-menu .menu-horizontal:has(> .szh-menu__submenu:only-child) {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.szh-menu .menu-horizontal > .szh-menu__item:not(:only-child):first-child, .szh-menu .menu-horizontal > .szh-menu__item:not(:only-child):first-child,

View file

@ -53,7 +53,7 @@ import { getAccessToken } from './utils/auth';
import focusDeck from './utils/focus-deck'; import focusDeck from './utils/focus-deck';
import states, { initStates, statusKey } from './utils/states'; import states, { initStates, statusKey } from './utils/states';
import store from './utils/store'; import store from './utils/store';
import { getCurrentAccount, setCurrentAccountID } from './utils/store-utils'; import { getCurrentAccount } from './utils/store-utils';
import './utils/toast-alert'; import './utils/toast-alert';
window.__STATES__ = states; window.__STATES__ = states;
@ -338,7 +338,7 @@ function App() {
window.__IGNORE_GET_ACCOUNT_ERROR__ = true; window.__IGNORE_GET_ACCOUNT_ERROR__ = true;
const account = getCurrentAccount(); const account = getCurrentAccount();
if (account) { if (account) {
setCurrentAccountID(account.info.id); store.session.set('currentAccount', account.info.id);
const { client } = api({ account }); const { client } = api({ account });
const { instance } = client; const { instance } = client;
// console.log('masto', masto); // console.log('masto', masto);

View file

@ -22,8 +22,7 @@ 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';
import store from '../utils/store'; import store from '../utils/store';
import { getCurrentAccountID, updateAccount } from '../utils/store-utils'; import { updateAccount } from '../utils/store-utils';
import supports from '../utils/supports';
import AccountBlock from './account-block'; import AccountBlock from './account-block';
import Avatar from './avatar'; import Avatar from './avatar';
@ -199,7 +198,10 @@ function AccountInfo({
} }
} }
const isSelf = useMemo(() => id === getCurrentAccountID(), [id]); const isSelf = useMemo(
() => id === store.session.get('currentAccount'),
[id],
);
useEffect(() => { useEffect(() => {
const infoHasEssentials = !!( const infoHasEssentials = !!(
@ -918,7 +920,7 @@ function RelatedActions({
useEffect(() => { useEffect(() => {
if (info) { if (info) {
const currentAccount = getCurrentAccountID(); const currentAccount = store.session.get('currentAccount');
let currentID; let currentID;
(async () => { (async () => {
if (sameInstance && authenticated) { if (sameInstance && authenticated) {
@ -1092,18 +1094,16 @@ function RelatedActions({
<Icon icon="translate" /> <Icon icon="translate" />
<span>Translate bio</span> <span>Translate bio</span>
</MenuItem> </MenuItem>
{supports('@mastodon/profile-private-note') && ( <MenuItem
<MenuItem onClick={() => {
onClick={() => { setShowPrivateNoteModal(true);
setShowPrivateNoteModal(true); }}
}} >
> <Icon icon="pencil" />
<Icon icon="pencil" /> <span>
<span> {privateNote ? 'Edit private note' : 'Add private note'}
{privateNote ? 'Edit private note' : 'Add private note'} </span>
</span> </MenuItem>
</MenuItem>
)}
{following && !!relationship && ( {following && !!relationship && (
<> <>
<MenuItem <MenuItem
@ -1452,22 +1452,19 @@ function RelatedActions({
</MenuItem> </MenuItem>
</> </>
)} )}
{currentAuthenticated && {currentAuthenticated && isSelf && standalone && (
isSelf && <>
standalone && <MenuDivider />
supports('@mastodon/profile-edit') && ( <MenuItem
<> onClick={() => {
<MenuDivider /> setShowEditProfile(true);
<MenuItem }}
onClick={() => { >
setShowEditProfile(true); <Icon icon="pencil" />
}} <span>Edit profile</span>
> </MenuItem>
<Icon icon="pencil" /> </>
<span>Edit profile</span> )}
</MenuItem>
</>
)}
{import.meta.env.DEV && currentAuthenticated && isSelf && ( {import.meta.env.DEV && currentAuthenticated && isSelf && (
<> <>
<MenuDivider /> <MenuDivider />

View file

@ -310,7 +310,7 @@
#compose-container .form-visibility-direct { #compose-container .form-visibility-direct {
--yellow-stripes: repeating-linear-gradient( --yellow-stripes: repeating-linear-gradient(
135deg, -45deg,
var(--reply-to-faded-color), var(--reply-to-faded-color),
var(--reply-to-faded-color) 10px, var(--reply-to-faded-color) 10px,
var(--reply-to-faded-color) 10px, var(--reply-to-faded-color) 10px,

View file

@ -124,14 +124,14 @@ const MENTION_RE = new RegExp(
// AI-generated, all other regexes are too complicated // AI-generated, all other regexes are too complicated
const HASHTAG_RE = new RegExp( const HASHTAG_RE = new RegExp(
`(^|[^=\\/\\w])(#[a-z0-9_]+([a-z0-9_.]+[a-z0-9_]+)?)(?![\\/\\w])`, `(^|[^=\\/\\w])(#[a-z0-9_]+([a-z0-9_.-]+[a-z0-9_]+)?)(?![\\/\\w])`,
'ig', 'ig',
); );
// https://github.com/mastodon/mastodon/blob/23e32a4b3031d1da8b911e0145d61b4dd47c4f96/app/models/custom_emoji.rb#L31 // https://github.com/mastodon/mastodon/blob/23e32a4b3031d1da8b911e0145d61b4dd47c4f96/app/models/custom_emoji.rb#L31
const SHORTCODE_RE_FRAGMENT = '[a-zA-Z0-9_]{2,}'; const SHORTCODE_RE_FRAGMENT = '[a-zA-Z0-9_]{2,}';
const SCAN_RE = new RegExp( const SCAN_RE = new RegExp(
`(^|[^=\\/\\w])(:${SHORTCODE_RE_FRAGMENT}:)(?=[^A-Za-z0-9_:]|$)`, `([^A-Za-z0-9_:\\n]|^)(:${SHORTCODE_RE_FRAGMENT}:)(?=[^A-Za-z0-9_:]|$)`,
'g', 'g',
); );
@ -988,11 +988,7 @@ function Compose({
} else { } else {
try { try {
newStatus = await masto.v1.statuses.create(params, { newStatus = await masto.v1.statuses.create(params, {
requestInit: { idempotencyKey: UID.current,
headers: {
'Idempotency-Key': UID.current,
},
},
}); });
} catch (_) { } catch (_) {
// If idempotency key fails, try again without it // If idempotency key fails, try again without it
@ -1219,30 +1215,22 @@ function Compose({
/> />
<Icon icon="attachment" /> <Icon icon="attachment" />
</label>{' '} </label>{' '}
{/* If maxOptions is not defined or defined and is greater than 1, show poll button */} <button
{maxOptions == null || type="button"
(maxOptions > 1 && ( class="toolbar-button"
<> disabled={
<button uiState === 'loading' || !!poll || !!mediaAttachments.length
type="button" }
class="toolbar-button" onClick={() => {
disabled={ setPoll({
uiState === 'loading' || options: ['', ''],
!!poll || expiresIn: 24 * 60 * 60, // 1 day
!!mediaAttachments.length multiple: false,
} });
onClick={() => { }}
setPoll({ >
options: ['', ''], <Icon icon="poll" alt="Add poll" />
expiresIn: 24 * 60 * 60, // 1 day </button>{' '}
multiple: false,
});
}}
>
<Icon icon="poll" alt="Add poll" />
</button>{' '}
</>
))}
<button <button
type="button" type="button"
class="toolbar-button" class="toolbar-button"
@ -2382,10 +2370,6 @@ function GIFPickerModal({ onClose = () => {}, onSelect = () => {} }) {
qRef.current?.focus(); qRef.current?.focus();
}, []); }, []);
const debouncedOnInput = useDebouncedCallback(() => {
fetchGIFs({ offset: 0 });
}, 1000);
return ( return (
<div id="gif-picker-sheet" class="sheet"> <div id="gif-picker-sheet" class="sheet">
{!!onClose && ( {!!onClose && (
@ -2412,7 +2396,6 @@ function GIFPickerModal({ onClose = () => {}, onSelect = () => {} }) {
autocapitalize="off" autocapitalize="off"
spellCheck="false" spellCheck="false"
dir="auto" dir="auto"
onInput={debouncedOnInput}
/> />
<input <input
type="image" type="image"

View file

@ -8,7 +8,6 @@ import FilterContext from '../utils/filter-context';
import { isFiltered } from '../utils/filters'; import { isFiltered } from '../utils/filters';
import states, { statusKey } from '../utils/states'; import states, { statusKey } from '../utils/states';
import store from '../utils/store'; import store from '../utils/store';
import { getCurrentAccountID } from '../utils/store-utils';
import Media from './media'; import Media from './media';
@ -89,7 +88,7 @@ function MediaPost({
}; };
const currentAccount = useMemo(() => { const currentAccount = useMemo(() => {
return getCurrentAccountID(); return store.session.get('currentAccount');
}, []); }, []);
const isSelf = useMemo(() => { const isSelf = useMemo(() => {
return currentAccount && currentAccount === accountId; return currentAccount && currentAccount === accountId;

View file

@ -341,15 +341,13 @@ function Media({
if (!hasDimensions) { if (!hasDimensions) {
const $media = e.target.closest('.media'); const $media = e.target.closest('.media');
if ($media) { if ($media) {
const { naturalWidth, naturalHeight } = e.target;
$media.dataset.orientation = $media.dataset.orientation =
naturalWidth > naturalHeight ? 'landscape' : 'portrait'; e.target.naturalWidth > e.target.naturalHeight
$media.style.setProperty('--width', `${naturalWidth}px`); ? 'landscape'
$media.style.setProperty( : 'portrait';
'--height', $media.style['--width'] = `${e.target.naturalWidth}px`;
`${naturalHeight}px`, $media.style['--height'] = `${e.target.naturalHeight}px`;
); $media.style.aspectRatio = `${e.target.naturalWidth}/${e.target.naturalHeight}`;
$media.style.aspectRatio = `${naturalWidth}/${naturalHeight}`;
} }
} }
}} }}
@ -388,7 +386,7 @@ function Media({
data-orientation="${orientation}" data-orientation="${orientation}"
preload="auto" preload="auto"
autoplay autoplay
${isGIF ? 'muted' : ''} muted="${isGIF}"
${isGIF ? '' : 'controls'} ${isGIF ? '' : 'controls'}
playsinline playsinline
loop="${loopable}" loop="${loopable}"
@ -513,25 +511,19 @@ function Media({
height={height} height={height}
data-orientation={orientation} data-orientation={orientation}
loading="lazy" loading="lazy"
decoding="async"
onLoad={(e) => { onLoad={(e) => {
if (!hasDimensions) { if (!hasDimensions) {
const $media = e.target.closest('.media'); const $media = e.target.closest('.media');
if ($media) { if ($media) {
const { naturalHeight, naturalWidth } = e.target;
$media.dataset.orientation = $media.dataset.orientation =
naturalWidth > naturalHeight e.target.naturalWidth > e.target.naturalHeight
? 'landscape' ? 'landscape'
: 'portrait'; : 'portrait';
$media.style.setProperty( $media.style['--width'] = `${e.target.naturalWidth}px`;
'--width', $media.style[
`${naturalWidth}px`, '--height'
); ] = `${e.target.naturalHeight}px`;
$media.style.setProperty( $media.style.aspectRatio = `${e.target.naturalWidth}/${e.target.naturalHeight}`;
'--height',
`${naturalHeight}px`,
);
$media.style.aspectRatio = `${naturalWidth}/${naturalHeight}`;
} }
} }
}} }}

View file

@ -11,8 +11,6 @@ import { getLists } from '../utils/lists';
import safeBoundingBoxPadding from '../utils/safe-bounding-box-padding'; import safeBoundingBoxPadding from '../utils/safe-bounding-box-padding';
import states from '../utils/states'; import states from '../utils/states';
import store from '../utils/store'; import store from '../utils/store';
import { getCurrentAccountID } from '../utils/store-utils';
import supports from '../utils/supports';
import Avatar from './avatar'; import Avatar from './avatar';
import Icon from './icon'; import Icon from './icon';
@ -26,8 +24,9 @@ function NavMenu(props) {
const [currentAccount, moreThanOneAccount] = useMemo(() => { const [currentAccount, moreThanOneAccount] = useMemo(() => {
const accounts = store.local.getJSON('accounts') || []; const accounts = store.local.getJSON('accounts') || [];
const acc = const acc =
accounts.find((account) => account.info.id === getCurrentAccountID()) || accounts.find(
accounts[0]; (account) => account.info.id === store.session.get('currentAccount'),
) || accounts[0];
return [acc, accounts.length > 1]; return [acc, accounts.length > 1];
}, []); }, []);
@ -84,10 +83,8 @@ function NavMenu(props) {
return results; return results;
} }
const supportsLists = supports('@mastodon/lists');
const [lists, setLists] = useState([]); const [lists, setLists] = useState([]);
useEffect(() => { useEffect(() => {
if (!supportsLists) return;
if (menuState === 'open') { if (menuState === 'open') {
getLists().then(setLists); getLists().then(setLists);
} }
@ -189,11 +186,9 @@ function NavMenu(props) {
<Icon icon="history2" size="l" /> <Icon icon="history2" size="l" />
<span>Catch-up</span> <span>Catch-up</span>
</MenuLink> </MenuLink>
{supports('@mastodon/mentions') && ( <MenuLink to="/mentions">
<MenuLink to="/mentions"> <Icon icon="at" size="l" /> <span>Mentions</span>
<Icon icon="at" size="l" /> <span>Mentions</span> </MenuLink>
</MenuLink>
)}
<MenuLink to="/notifications"> <MenuLink to="/notifications">
<Icon icon="notification" size="l" /> <span>Notifications</span> <Icon icon="notification" size="l" /> <span>Notifications</span>
{snapStates.notificationsShowNew && ( {snapStates.notificationsShowNew && (
@ -237,12 +232,10 @@ function NavMenu(props) {
)} )}
</SubMenu2> </SubMenu2>
) : ( ) : (
supportsLists && ( <MenuLink to="/l">
<MenuLink to="/l"> <Icon icon="list" size="l" />
<Icon icon="list" size="l" /> <span>Lists</span>
<span>Lists</span> </MenuLink>
</MenuLink>
)
)} )}
<MenuLink to="/b"> <MenuLink to="/b">
<Icon icon="bookmark" size="l" /> <span>Bookmarks</span> <Icon icon="bookmark" size="l" /> <span>Bookmarks</span>
@ -267,12 +260,10 @@ function NavMenu(props) {
<span>Followed Hashtags</span> <span>Followed Hashtags</span>
</MenuLink> </MenuLink>
<MenuDivider /> <MenuDivider />
{supports('@mastodon/filters') && ( <MenuLink to="/ft">
<MenuLink to="/ft"> <Icon icon="filters" size="l" />
<Icon icon="filters" size="l" /> Filters
Filters </MenuLink>
</MenuLink>
)}
<MenuItem <MenuItem
onClick={() => { onClick={() => {
states.showGenericAccounts = { states.showGenericAccounts = {

View file

@ -4,7 +4,6 @@ import { memo } from 'preact/compat';
import shortenNumber from '../utils/shorten-number'; import shortenNumber from '../utils/shorten-number';
import states, { statusKey } from '../utils/states'; import states, { statusKey } from '../utils/states';
import store from '../utils/store'; import store from '../utils/store';
import { getCurrentAccountID } from '../utils/store-utils';
import useTruncated from '../utils/useTruncated'; import useTruncated from '../utils/useTruncated';
import Avatar from './avatar'; import Avatar from './avatar';
@ -133,7 +132,7 @@ function Notification({
const actualStatus = status?.reblog || status; const actualStatus = status?.reblog || status;
const actualStatusID = actualStatus?.id; const actualStatusID = actualStatus?.id;
const currentAccount = getCurrentAccountID(); const currentAccount = store.session.get('currentAccount');
const isSelf = currentAccount === account?.id; const isSelf = currentAccount === account?.id;
const isVoted = status?.poll?.voted; const isVoted = status?.poll?.voted;
const isReplyToOthers = const isReplyToOthers =

View file

@ -21,7 +21,6 @@ export default function RelativeTime({ datetime, format }) {
const [renderCount, rerender] = useReducer((x) => x + 1, 0); const [renderCount, rerender] = useReducer((x) => x + 1, 0);
const date = useMemo(() => dayjs(datetime), [datetime]); const date = useMemo(() => dayjs(datetime), [datetime]);
const [dateStr, dt, title] = useMemo(() => { const [dateStr, dt, title] = useMemo(() => {
if (!date.isValid()) return ['' + datetime, '', ''];
let str; let str;
if (format === 'micro') { if (format === 'micro') {
// If date <= 1 day ago or day is within this year // If date <= 1 day ago or day is within this year
@ -38,7 +37,6 @@ export default function RelativeTime({ datetime, format }) {
}, [date, format, renderCount]); }, [date, format, renderCount]);
useEffect(() => { useEffect(() => {
if (!date.isValid()) return;
let timeout; let timeout;
let raf; let raf;
function rafRerender() { function rafRerender() {

View file

@ -19,7 +19,6 @@ 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';
import store from '../utils/store'; import store from '../utils/store';
import { getCurrentAccountID } from '../utils/store-utils';
import AsyncText from './AsyncText'; import AsyncText from './AsyncText';
import Icon from './icon'; import Icon from './icon';
@ -788,7 +787,7 @@ function ImportExport({ shortcuts, onClose }) {
disabled={importUIState === 'cloud-downloading'} disabled={importUIState === 'cloud-downloading'}
onClick={async () => { onClick={async () => {
setImportUIState('cloud-downloading'); setImportUIState('cloud-downloading');
const currentAccount = getCurrentAccountID(); const currentAccount = store.session.get('currentAccount');
showToast( showToast(
'Downloading saved shortcuts from instance server…', 'Downloading saved shortcuts from instance server…',
); );
@ -1044,7 +1043,7 @@ function ImportExport({ shortcuts, onClose }) {
disabled={importUIState === 'cloud-uploading'} disabled={importUIState === 'cloud-uploading'}
onClick={async () => { onClick={async () => {
setImportUIState('cloud-uploading'); setImportUIState('cloud-uploading');
const currentAccount = getCurrentAccountID(); const currentAccount = store.session.get('currentAccount');
try { try {
const relationships = const relationships =
await masto.v1.accounts.relationships.fetch({ await masto.v1.accounts.relationships.fetch({

View file

@ -47,7 +47,7 @@
} }
.visibility-direct { .visibility-direct {
--yellow-stripes: repeating-linear-gradient( --yellow-stripes: repeating-linear-gradient(
135deg, -45deg,
var(--reply-to-faded-color), var(--reply-to-faded-color),
var(--reply-to-faded-color) 10px, var(--reply-to-faded-color) 10px,
var(--reply-to-faded-color) 10px, var(--reply-to-faded-color) 10px,
@ -365,10 +365,6 @@
background-image: var(--yellow-stripes); background-image: var(--yellow-stripes);
} }
.status-pre-meta + & {
background-image: none;
}
> * { > * {
opacity: 0.65; opacity: 0.65;
transition: opacity 1s ease-out; transition: opacity 1s ease-out;
@ -1324,22 +1320,11 @@ body:has(#modal-container .carousel) .status .media img:hover {
} }
.status.skeleton .media-first-container { .status.skeleton .media-first-container {
min-height: 320px; min-height: 3em;
background-color: var(--outline-color); background-color: var(--outline-color);
} }
@keyframes media-carousel-slide {
0% {
transform: translateX(calc(var(--dots-count, 1) * 2.5px));
}
100% {
transform: translateX(calc(var(--dots-count, 1) * -2.5px));
}
}
.status-media-first { .status-media-first {
timeline-scope: --media-carousel;
.meta-name { .meta-name {
opacity: 0.65; opacity: 0.65;
transition: opacity 0.5s ease-in-out; transition: opacity 0.5s ease-in-out;
@ -1371,20 +1356,76 @@ body:has(#modal-container .carousel) .status .media img:hover {
.media-first-spoiler-button { .media-first-spoiler-button {
display: inline-flex !important; display: inline-flex !important;
} }
.media-first-container { .media-first-container {
position: relative;
margin-top: 8px; margin-top: 8px;
display: flex;
max-height: 80vh;
overflow-x: auto;
overflow-y: hidden;
scroll-snap-type: x mandatory;
scroll-behavior: smooth;
user-select: none;
margin-inline: -16px; margin-inline: -16px;
position: relative;
scrollbar-width: none;
/* border: var(--hairline-width) solid var(--outline-color);
border-inline-width: 0;
background-color: var(--bg-faded-color); */
@media (min-width: 40em) { @media (min-width: 40em) {
margin-inline: 0; margin-inline: 0;
/* border-radius: 4px; */
border-inline-width: var(--hairline-width);
}
&::-webkit-scrollbar {
display: none;
}
> .media-first-item {
scroll-snap-align: center;
scroll-snap-stop: always;
flex-shrink: 0;
display: flex;
width: 100%;
align-items: center;
justify-content: center;
&:not(:only-child) {
background-color: var(--bg-blur-color);
box-shadow: inset 0 0 0 var(--hairline-width) var(--outline-color);
}
.media {
/* background-color: var(--average-color, var(--bg-faded-color)); */
width: var(--width);
max-width: 100%;
max-height: 100%;
min-height: var(--min-dimension);
/* max-height: min(var(--height), 80vh); */
&:active {
transform: none;
}
img,
video {
object-fit: scale-down;
animation: none;
&:not([data-loaded='true']) {
background-color: var(--bg-color);
}
}
}
} }
.media-carousel-controls { .media-carousel-controls {
flex-shrink: 0; flex-shrink: 0;
position: absolute; width: 100%;
inset: 0; position: sticky;
right: 0;
left: 0;
pointer-events: none; pointer-events: none;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
@ -1402,7 +1443,7 @@ body:has(#modal-container .carousel) .status .media img:hover {
font-size: 0.8em; font-size: 0.8em;
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
opacity: 0.6; opacity: 0.6;
transition: opacity 1s ease-in-out 0.3s; transition: opacity 1.5s ease-in-out;
border: var(--hairline-width) solid var(--media-outline-color); border: var(--hairline-width) solid var(--media-outline-color);
} }
@ -1436,76 +1477,6 @@ body:has(#modal-container .carousel) .status .media img:hover {
} }
} }
} }
.media-first-carousel {
display: flex;
max-height: 80vh;
overflow-x: auto;
overflow-y: hidden;
scroll-snap-type: x mandatory;
scroll-behavior: smooth;
user-select: none;
scrollbar-width: none;
/* border: var(--hairline-width) solid var(--outline-color);
border-inline-width: 0;
background-color: var(--bg-faded-color); */
box-shadow: 0 0 0 var(--hairline-width) var(--outline-color);
scroll-timeline: --media-carousel x;
@media (min-width: 40em) {
/* margin-inline: 0; */
/* border-radius: 4px; */
/* border-inline-width: var(--hairline-width); */
box-shadow: none;
}
&::-webkit-scrollbar {
display: none;
}
> .media-first-item {
scroll-snap-align: center;
scroll-snap-stop: always;
flex-shrink: 0;
display: flex;
width: 100%;
align-items: center;
justify-content: center;
&:not(:only-child) {
background-color: var(--bg-blur-color);
/* box-shadow: inset 0 0 0 var(--hairline-width) var(--outline-color); */
}
.media {
/* background-color: var(--average-color, var(--bg-faded-color)); */
width: var(--width, 100%);
max-width: 100%;
max-height: 100%;
min-height: var(--min-dimension);
/* max-height: min(var(--height), 80vh); */
&:has(img:not([data-loaded='true'])) {
min-height: 320px;
}
&:active {
transform: none;
filter: none;
}
img,
video {
object-fit: scale-down;
animation: none;
&:not([data-loaded='true']) {
background-color: var(--bg-color);
}
}
}
}
}
:is(:hover, :focus) > & .carousel-indexer { :is(:hover, :focus) > & .carousel-indexer {
opacity: 0; opacity: 0;
} }
@ -1518,11 +1489,6 @@ body:has(#modal-container .carousel) .status .media img:hover {
margin-top: 8px; margin-top: 8px;
padding: 8px; padding: 8px;
@supports (animation-timeline: scroll()) {
animation: media-carousel-slide 1s linear both;
animation-timeline: --media-carousel;
}
.carousel-dot { .carousel-dot {
display: inline-block; display: inline-block;
width: 5px; width: 5px;
@ -1531,7 +1497,6 @@ body:has(#modal-container .carousel) .status .media img:hover {
background-color: var(--text-color); background-color: var(--text-color);
transition: all 0.3s ease-in-out; transition: all 0.3s ease-in-out;
opacity: 0.3; opacity: 0.3;
flex-shrink: 0;
&.active { &.active {
opacity: 1; opacity: 1;

View file

@ -11,7 +11,6 @@ import {
import { decodeBlurHash, getBlurHashAverageColor } from 'fast-blurhash'; import { decodeBlurHash, getBlurHashAverageColor } from 'fast-blurhash';
import { shallowEqual } from 'fast-equals'; import { shallowEqual } from 'fast-equals';
import prettify from 'html-prettify'; import prettify from 'html-prettify';
import { Fragment } from 'preact';
import { memo } from 'preact/compat'; import { memo } from 'preact/compat';
import { import {
useCallback, useCallback,
@ -56,8 +55,6 @@ import { speak, supportsTTS } from '../utils/speech';
import states, { getStatus, saveStatus, statusKey } from '../utils/states'; import states, { getStatus, saveStatus, statusKey } from '../utils/states';
import statusPeek from '../utils/status-peek'; import statusPeek from '../utils/status-peek';
import store from '../utils/store'; import store from '../utils/store';
import { getCurrentAccountID } from '../utils/store-utils';
import supports from '../utils/supports';
import unfurlMastodonLink from '../utils/unfurl-link'; import unfurlMastodonLink from '../utils/unfurl-link';
import useTruncated from '../utils/useTruncated'; import useTruncated from '../utils/useTruncated';
import visibilityIconsMap from '../utils/visibility-icons-map'; import visibilityIconsMap from '../utils/visibility-icons-map';
@ -151,12 +148,6 @@ const PostContent = memo(
}, },
); );
const SIZE_CLASS = {
s: 'small',
m: 'medium',
l: 'large',
};
function Status({ function Status({
statusID, statusID,
status, status,
@ -182,11 +173,7 @@ function Status({
}) { }) {
if (skeleton) { if (skeleton) {
return ( return (
<div <div class={`status skeleton ${mediaFirst ? 'status-media-first' : ''}`}>
class={`status skeleton ${
mediaFirst ? 'status-media-first small' : ''
}`}
>
{!mediaFirst && <Avatar size="xxl" />} {!mediaFirst && <Avatar size="xxl" />}
<div class="container"> <div class="container">
<div class="meta"> <div class="meta">
@ -269,7 +256,7 @@ function Status({
if (mediaFirst && hasMediaAttachments) size = 's'; if (mediaFirst && hasMediaAttachments) size = 's';
const currentAccount = useMemo(() => { const currentAccount = useMemo(() => {
return getCurrentAccountID(); return store.session.get('currentAccount');
}, []); }, []);
const isSelf = useMemo(() => { const isSelf = useMemo(() => {
return currentAccount && currentAccount === accountId; return currentAccount && currentAccount === accountId;
@ -407,8 +394,8 @@ function Status({
} }
// Check followedTags // Check followedTags
const FollowedTagsParent = useCallback( if (showFollowedTags && !!snapStates.statusFollowedTags[sKey]?.length) {
({ children }) => ( return (
<div <div
data-state-post-id={sKey} data-state-post-id={sKey}
class="status-followed-tags" class="status-followed-tags"
@ -426,15 +413,19 @@ function Status({
</Link> </Link>
))} ))}
</div> </div>
{children} <Status
status={statusID ? null : status}
statusID={statusID ? status.id : null}
instance={instance}
size={size}
contentTextWeight={contentTextWeight}
readOnly={readOnly}
enableCommentHint
mediaFirst={mediaFirst}
/>
</div> </div>
), );
[sKey, instance, snapStates.statusFollowedTags[sKey]], }
);
const StatusParent =
showFollowedTags && !!snapStates.statusFollowedTags[sKey]?.length
? FollowedTagsParent
: Fragment;
const isSizeLarge = size === 'l'; const isSizeLarge = size === 'l';
@ -648,7 +639,6 @@ function Status({
}; };
const bookmarkStatus = async () => { const bookmarkStatus = async () => {
if (!supports('@mastodon/post-bookmark')) return;
if (!sameInstance || !authenticated) { if (!sameInstance || !authenticated) {
alert(unauthInteractionErrorMessage); alert(unauthInteractionErrorMessage);
return false; return false;
@ -836,15 +826,13 @@ function Status({
: 'Like'} : 'Like'}
</span> </span>
</MenuItem> </MenuItem>
{supports('@mastodon/post-bookmark') && ( <MenuItem
<MenuItem onClick={bookmarkStatusNotify}
onClick={bookmarkStatusNotify} className={`menu-bookmark ${bookmarked ? 'checked' : ''}`}
className={`menu-bookmark ${bookmarked ? 'checked' : ''}`} >
> <Icon icon="bookmark" />
<Icon icon="bookmark" /> <span>{bookmarked ? 'Unbookmark' : 'Bookmark'}</span>
<span>{bookmarked ? 'Unbookmark' : 'Bookmark'}</span> </MenuItem>
</MenuItem>
)}
</div> </div>
</> </>
)} )}
@ -1088,18 +1076,16 @@ function Status({
)} )}
{isSelf && ( {isSelf && (
<div class="menu-horizontal"> <div class="menu-horizontal">
{supports('@mastodon/post-edit') && ( <MenuItem
<MenuItem onClick={() => {
onClick={() => { states.showCompose = {
states.showCompose = { editStatus: status,
editStatus: status, };
}; }}
}} >
> <Icon icon="pencil" />
<Icon icon="pencil" /> <span>Edit</span>
<span>Edit</span> </MenuItem>
</MenuItem>
)}
{isSizeLarge && ( {isSizeLarge && (
<MenuConfirm <MenuConfirm
subMenu subMenu
@ -1380,7 +1366,7 @@ function Status({
]); ]);
return ( return (
<StatusParent> <>
{showReplyParent && !!(inReplyToId && inReplyToAccountId) && ( {showReplyParent && !!(inReplyToId && inReplyToAccountId) && (
<StatusCompact sKey={sKey} /> <StatusCompact sKey={sKey} />
)} )}
@ -1408,7 +1394,11 @@ function Status({
? 'status-reply-to' ? 'status-reply-to'
: '' : ''
} visibility-${visibility} ${_pinned ? 'status-pinned' : ''} ${ } visibility-${visibility} ${_pinned ? 'status-pinned' : ''} ${
SIZE_CLASS[size] {
s: 'small',
m: 'medium',
l: 'large',
}[size]
} ${_deleted ? 'status-deleted' : ''} ${quoted ? 'status-card' : ''} ${ } ${_deleted ? 'status-deleted' : ''} ${quoted ? 'status-card' : ''} ${
isContextMenuOpen ? 'status-menu-open' : '' isContextMenuOpen ? 'status-menu-open' : ''
} ${mediaFirst && hasMediaAttachments ? 'status-media-first' : ''}`} } ${mediaFirst && hasMediaAttachments ? 'status-media-first' : ''}`}
@ -2169,18 +2159,16 @@ function Status({
onClick={favouriteStatus} onClick={favouriteStatus}
/> />
</div> </div>
{supports('@mastodon/post-bookmark') && ( <div class="action">
<div class="action"> <StatusButton
<StatusButton checked={bookmarked}
checked={bookmarked} title={['Bookmark', 'Unbookmark']}
title={['Bookmark', 'Unbookmark']} alt={['Bookmark', 'Bookmarked']}
alt={['Bookmark', 'Bookmarked']} class="bookmark-button"
class="bookmark-button" icon="bookmark"
icon="bookmark" onClick={bookmarkStatus}
onClick={bookmarkStatus} />
/> </div>
</div>
)}
<Menu2 <Menu2
portal={{ portal={{
target: target:
@ -2248,7 +2236,7 @@ function Status({
</Modal> </Modal>
)} )}
</article> </article>
</StatusParent> </>
); );
} }
@ -2292,18 +2280,16 @@ function MediaFirstContainer(props) {
return ( return (
<> <>
<div class="media-first-container"> <div class="media-first-container" ref={carouselRef}>
<div class="media-first-carousel" ref={carouselRef}> {mediaAttachments.map((media, i) => (
{mediaAttachments.map((media, i) => ( <div class="media-first-item" key={media.id}>
<div class="media-first-item" key={media.id}> <Media
<Media media={media}
media={media} lang={language}
lang={language} to={`/${instance}/s/${postID}?media-only=${i + 1}`}
to={`/${instance}/s/${postID}?media=${i + 1}`} />
/> </div>
</div> ))}
))}
</div>
{moreThanOne && ( {moreThanOne && (
<div class="media-carousel-controls"> <div class="media-carousel-controls">
<div class="carousel-indexer"> <div class="carousel-indexer">
@ -2349,12 +2335,7 @@ function MediaFirstContainer(props) {
)} )}
</div> </div>
{moreThanOne && ( {moreThanOne && (
<div <div class="media-carousel-dots">
class="media-carousel-dots"
style={{
'--dots-count': mediaAttachments.length,
}}
>
{mediaAttachments.map((media, i) => ( {mediaAttachments.map((media, i) => (
<span <span
key={media.id} key={media.id}

View file

@ -21,7 +21,6 @@ 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';
import { saveStatus } from '../utils/states'; import { saveStatus } from '../utils/states';
import { isMediaFirstInstance } from '../utils/store-utils';
import useTitle from '../utils/useTitle'; import useTitle from '../utils/useTitle';
const LIMIT = 20; const LIMIT = 20;
@ -69,8 +68,6 @@ function AccountStatuses() {
searchOffsetRef.current = 0; searchOffsetRef.current = 0;
}, allSearchParams); }, allSearchParams);
const mediaFirst = useMemo(() => isMediaFirstInstance(), []);
const sameCurrentInstance = useMemo( const sameCurrentInstance = useMemo(
() => instance === currentInstance, () => instance === currentInstance,
[instance, currentInstance], [instance, currentInstance],
@ -189,7 +186,7 @@ function AccountStatuses() {
limit: LIMIT, limit: LIMIT,
exclude_replies: excludeReplies, exclude_replies: excludeReplies,
exclude_reblogs: excludeBoosts, exclude_reblogs: excludeBoosts,
only_media: media || undefined, only_media: media,
tagged, tagged,
}); });
} }
@ -273,21 +270,17 @@ function AccountStatuses() {
} catch (e) { } catch (e) {
console.error(e); console.error(e);
} }
// No need, because the whole filter bar is hidden try {
// TODO: Revisit this const featuredTags = await masto.v1.accounts
if (!mediaFirst) { .$select(id)
try { .featuredTags.list();
const featuredTags = await masto.v1.accounts console.log({ featuredTags });
.$select(id) setFeaturedTags(featuredTags);
.featuredTags.list(); } catch (e) {
console.log({ featuredTags }); console.error(e);
setFeaturedTags(featuredTags);
} catch (e) {
console.error(e);
}
} }
})(); })();
}, [id, mediaFirst]); }, [id]);
const { displayName, acct, emojis } = account || {}; const { displayName, acct, emojis } = account || {};
@ -306,126 +299,95 @@ function AccountStatuses() {
authenticated={authenticated} authenticated={authenticated}
standalone standalone
/> />
{!mediaFirst && ( <div
<div class="filter-bar"
class="filter-bar" ref={filterBarRef}
ref={filterBarRef} style={{
style={{ position: 'relative',
position: 'relative', }}
>
{filtered ? (
<Link
to={`/${instance}/a/${id}`}
class="insignificant filter-clear"
title="Clear filters"
key="clear-filters"
>
<Icon icon="x" size="l" />
</Link>
) : (
<Icon icon="filter" class="insignificant" size="l" />
)}
<Link
to={`/${instance}/a/${id}${excludeReplies ? '?replies=1' : ''}`}
onClick={() => {
if (excludeReplies) {
showToast('Showing post with replies');
}
}} }}
class={excludeReplies ? '' : 'is-active'}
> >
{filtered ? ( + Replies
<Link </Link>
to={`/${instance}/a/${id}`} <Link
class="insignificant filter-clear" to={`/${instance}/a/${id}${excludeBoosts ? '' : '?boosts=0'}`}
title="Clear filters" onClick={() => {
key="clear-filters" if (!excludeBoosts) {
> showToast('Showing posts without boosts');
<Icon icon="x" size="l" /> }
</Link> }}
) : ( class={!excludeBoosts ? '' : 'is-active'}
<Icon icon="filter" class="insignificant" size="l" /> >
)} - Boosts
</Link>
<Link
to={`/${instance}/a/${id}${media ? '' : '?media=1'}`}
onClick={() => {
if (!media) {
showToast('Showing posts with media');
}
}}
class={media ? 'is-active' : ''}
>
Media
</Link>
{featuredTags.map((tag) => (
<Link <Link
to={`/${instance}/a/${id}${excludeReplies ? '?replies=1' : ''}`} key={tag.id}
to={`/${instance}/a/${id}${
tagged === tag.name
? ''
: `?tagged=${encodeURIComponent(tag.name)}`
}`}
onClick={() => { onClick={() => {
if (excludeReplies) { if (tagged !== tag.name) {
showToast('Showing post with replies'); showToast(`Showing posts tagged with #${tag.name}`);
} }
}} }}
class={excludeReplies ? '' : 'is-active'} class={tagged === tag.name ? 'is-active' : ''}
> >
+ Replies <span>
<span class="more-insignificant">#</span>
{tag.name}
</span>
{
// The count differs based on instance 😅
}
{/* <span class="filter-count">{tag.statusesCount}</span> */}
</Link> </Link>
<Link ))}
to={`/${instance}/a/${id}${excludeBoosts ? '' : '?boosts=0'}`} {searchEnabled &&
onClick={() => { (supportsInputMonth ? (
if (!excludeBoosts) { <label class={`filter-field ${month ? 'is-active' : ''}`}>
showToast('Showing posts without boosts'); <Icon icon="month" size="l" />
} <input
}} type="month"
class={!excludeBoosts ? '' : 'is-active'}
>
- Boosts
</Link>
<Link
to={`/${instance}/a/${id}${media ? '' : '?media=1'}`}
onClick={() => {
if (!media) {
showToast('Showing posts with media');
}
}}
class={media ? 'is-active' : ''}
>
Media
</Link>
{featuredTags.map((tag) => (
<Link
key={tag.id}
to={`/${instance}/a/${id}${
tagged === tag.name
? ''
: `?tagged=${encodeURIComponent(tag.name)}`
}`}
onClick={() => {
if (tagged !== tag.name) {
showToast(`Showing posts tagged with #${tag.name}`);
}
}}
class={tagged === tag.name ? 'is-active' : ''}
>
<span>
<span class="more-insignificant">#</span>
{tag.name}
</span>
{
// The count differs based on instance 😅
}
{/* <span class="filter-count">{tag.statusesCount}</span> */}
</Link>
))}
{searchEnabled &&
(supportsInputMonth ? (
<label class={`filter-field ${month ? 'is-active' : ''}`}>
<Icon icon="month" size="l" />
<input
type="month"
disabled={!account?.acct}
value={month || ''}
min={MIN_YEAR_MONTH}
max={new Date().toISOString().slice(0, 7)}
onInput={(e) => {
const { value, validity } = e.currentTarget;
if (!validity.valid) return;
setSearchParams(
value
? {
month: value,
}
: {},
);
const [year, month] = value.split('-');
const monthIndex = parseInt(month, 10) - 1;
const date = new Date(year, monthIndex);
showToast(
`Showing posts in ${date.toLocaleString('default', {
month: 'long',
year: 'numeric',
})}`,
);
}}
/>
</label>
) : (
// Fallback to <select> for month and <input type="number"> for year
<MonthPicker
class={`filter-field ${month ? 'is-active' : ''}`}
disabled={!account?.acct} disabled={!account?.acct}
value={month || ''} value={month || ''}
min={MIN_YEAR_MONTH} min={MIN_YEAR_MONTH}
max={new Date().toISOString().slice(0, 7)} max={new Date().toISOString().slice(0, 7)}
onInput={(e) => { onInput={(e) => {
const { value, validity } = e; const { value, validity } = e.currentTarget;
if (!validity.valid) return; if (!validity.valid) return;
setSearchParams( setSearchParams(
value value
@ -434,11 +396,40 @@ function AccountStatuses() {
} }
: {}, : {},
); );
const [year, month] = value.split('-');
const monthIndex = parseInt(month, 10) - 1;
const date = new Date(year, monthIndex);
showToast(
`Showing posts in ${date.toLocaleString('default', {
month: 'long',
year: 'numeric',
})}`,
);
}} }}
/> />
))} </label>
</div> ) : (
)} // Fallback to <select> for month and <input type="number"> for year
<MonthPicker
class={`filter-field ${month ? 'is-active' : ''}`}
disabled={!account?.acct}
value={month || ''}
min={MIN_YEAR_MONTH}
max={new Date().toISOString().slice(0, 7)}
onInput={(e) => {
const { value, validity } = e;
if (!validity.valid) return;
setSearchParams(
value
? {
month: value,
}
: {},
);
}}
/>
))}
</div>
</> </>
); );
}, [ }, [
@ -501,7 +492,7 @@ function AccountStatuses() {
errorText="Unable to load posts" errorText="Unable to load posts"
fetchItems={fetchAccountStatuses} fetchItems={fetchAccountStatuses}
useItemID useItemID
view={media || mediaFirst ? 'media' : undefined} view={media ? 'media' : undefined}
boostsCarousel={snapStates.settings.boostsCarousel} boostsCarousel={snapStates.settings.boostsCarousel}
timelineStart={TimelineStart} timelineStart={TimelineStart}
refresh={[ refresh={[

View file

@ -13,13 +13,12 @@ import NameText from '../components/name-text';
import { api } from '../utils/api'; import { api } from '../utils/api';
import states from '../utils/states'; import states from '../utils/states';
import store from '../utils/store'; import store from '../utils/store';
import { getCurrentAccountID, setCurrentAccountID } from '../utils/store-utils';
function Accounts({ onClose }) { function Accounts({ onClose }) {
const { masto } = api(); const { masto } = api();
// Accounts // Accounts
const accounts = store.local.getJSON('accounts'); const accounts = store.local.getJSON('accounts');
const currentAccount = getCurrentAccountID(); const currentAccount = store.session.get('currentAccount');
const moreThanOneAccount = accounts.length > 1; const moreThanOneAccount = accounts.length > 1;
const [_, reload] = useReducer((x) => x + 1, 0); const [_, reload] = useReducer((x) => x + 1, 0);
@ -82,7 +81,7 @@ function Accounts({ onClose }) {
if (isCurrent) { if (isCurrent) {
states.showAccount = `${account.info.username}@${account.instanceURL}`; states.showAccount = `${account.info.username}@${account.instanceURL}`;
} else { } else {
setCurrentAccountID(account.info.id); store.session.set('currentAccount', account.info.id);
location.reload(); location.reload();
} }
}} }}

View file

@ -614,7 +614,7 @@
} }
&.visibility-direct { &.visibility-direct {
--yellow-stripes: repeating-linear-gradient( --yellow-stripes: repeating-linear-gradient(
135deg, -45deg,
var(--reply-to-faded-color), var(--reply-to-faded-color),
var(--reply-to-faded-color) 10px, var(--reply-to-faded-color) 10px,
var(--reply-to-faded-color) 10px, var(--reply-to-faded-color) 10px,

View file

@ -40,7 +40,7 @@ import showToast from '../utils/show-toast';
import states, { statusKey } from '../utils/states'; import states, { statusKey } from '../utils/states';
import statusPeek from '../utils/status-peek'; import statusPeek from '../utils/status-peek';
import store from '../utils/store'; import store from '../utils/store';
import { getCurrentAccountID, getCurrentAccountNS } from '../utils/store-utils'; import { getCurrentAccountNS } from '../utils/store-utils';
import { assignFollowedTags } from '../utils/timeline-utils'; import { assignFollowedTags } from '../utils/timeline-utils';
import useTitle from '../utils/useTitle'; import useTitle from '../utils/useTitle';
@ -112,7 +112,7 @@ function Catchup() {
const [showTopLinks, setShowTopLinks] = useState(false); const [showTopLinks, setShowTopLinks] = useState(false);
const currentAccount = useMemo(() => { const currentAccount = useMemo(() => {
return getCurrentAccountID(); return store.session.get('currentAccount');
}, []); }, []);
const isSelf = (accountID) => accountID === currentAccount; const isSelf = (accountID) => accountID === currentAccount;

View file

@ -5,7 +5,7 @@ import {
MenuHeader, MenuHeader,
MenuItem, MenuItem,
} from '@szhsin/react-menu'; } from '@szhsin/react-menu';
import { useEffect, useMemo, useRef, useState } from 'preact/hooks'; import { useEffect, useRef, useState } from 'preact/hooks';
import { useNavigate, useParams, useSearchParams } from 'react-router-dom'; import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
import Icon from '../components/icon'; import Icon from '../components/icon';
@ -18,7 +18,6 @@ import { filteredItems } from '../utils/filters';
import showToast from '../utils/show-toast'; import showToast from '../utils/show-toast';
import states from '../utils/states'; import states from '../utils/states';
import { saveStatus } from '../utils/states'; import { saveStatus } from '../utils/states';
import { isMediaFirstInstance } from '../utils/store-utils';
import useTitle from '../utils/useTitle'; import useTitle from '../utils/useTitle';
const LIMIT = 20; const LIMIT = 20;
@ -56,8 +55,6 @@ function Hashtags({ media: mediaView, columnMode, ...props }) {
useTitle(title, `/:instance?/t/:hashtag`); useTitle(title, `/:instance?/t/:hashtag`);
const latestItem = useRef(); const latestItem = useRef();
const mediaFirst = useMemo(() => isMediaFirstInstance(), []);
// const hashtagsIterator = useRef(); // const hashtagsIterator = useRef();
const maxID = useRef(undefined); const maxID = useRef(undefined);
async function fetchHashtags(firstLoad) { async function fetchHashtags(firstLoad) {
@ -76,7 +73,7 @@ function Hashtags({ media: mediaView, columnMode, ...props }) {
limit: LIMIT, limit: LIMIT,
any: hashtags.slice(1), any: hashtags.slice(1),
maxId: firstLoad ? undefined : maxID.current, maxId: firstLoad ? undefined : maxID.current,
onlyMedia: media ? true : undefined, onlyMedia: media,
}) })
.next(); .next();
let { value } = results; let { value } = results;
@ -88,7 +85,7 @@ function Hashtags({ media: mediaView, columnMode, ...props }) {
// value = filteredItems(value, 'public'); // value = filteredItems(value, 'public');
value.forEach((item) => { value.forEach((item) => {
saveStatus(item, instance, { saveStatus(item, instance, {
skipThreading: media || mediaFirst, // If media view, no need to form threads skipThreading: media, // If media view, no need to form threads
}); });
}); });
@ -159,7 +156,7 @@ function Hashtags({ media: mediaView, columnMode, ...props }) {
fetchItems={fetchHashtags} fetchItems={fetchHashtags}
checkForUpdates={checkForUpdates} checkForUpdates={checkForUpdates}
useItemID useItemID
view={media || mediaFirst ? 'media' : undefined} view={media ? 'media' : undefined}
refresh={media} refresh={media}
// allowFilters // allowFilters
filterContext="public" filterContext="public"
@ -236,27 +233,23 @@ function Hashtags({ media: mediaView, columnMode, ...props }) {
<MenuDivider /> <MenuDivider />
</> </>
)} )}
{!mediaFirst && ( <MenuHeader className="plain">Filters</MenuHeader>
<> <MenuItem
<MenuHeader className="plain">Filters</MenuHeader> type="checkbox"
<MenuItem checked={!!media}
type="checkbox" onClick={() => {
checked={!!media} if (media) {
onClick={() => { searchParams.delete('media');
if (media) { } else {
searchParams.delete('media'); searchParams.set('media', '1');
} else { }
searchParams.set('media', '1'); setSearchParams(searchParams);
} }}
setSearchParams(searchParams); >
}} <Icon icon="check-circle" />{' '}
> <span class="menu-grow">Media only</span>
<Icon icon="check-circle" />{' '} </MenuItem>
<span class="menu-grow">Media only</span> <MenuDivider />
</MenuItem>
<MenuDivider />
</>
)}
<FocusableItem className="menu-field" disabled={reachLimit}> <FocusableItem className="menu-field" disabled={reachLimit}>
{({ ref }) => ( {({ ref }) => (
<form <form

View file

@ -4,7 +4,6 @@ import { useSearchParams } from 'react-router-dom';
import Link from '../components/link'; import Link from '../components/link';
import Timeline from '../components/timeline'; import Timeline from '../components/timeline';
import { api } from '../utils/api'; import { api } from '../utils/api';
import { fixNotifications } from '../utils/group-notifications';
import { saveStatus } from '../utils/states'; import { saveStatus } from '../utils/states';
import useTitle from '../utils/useTitle'; import useTitle from '../utils/useTitle';
@ -31,8 +30,6 @@ function Mentions({ columnMode, ...props }) {
const results = await mentionsIterator.current.next(); const results = await mentionsIterator.current.next();
let { value } = results; let { value } = results;
if (value?.length) { if (value?.length) {
value = fixNotifications(value);
if (firstLoad) { if (firstLoad) {
latestItem.current = value[0].id; latestItem.current = value[0].id;
console.log('First load', latestItem.current); console.log('First load', latestItem.current);

View file

@ -72,13 +72,6 @@ function Notifications({ columnMode }) {
excludeTypes: ['follow_request'], excludeTypes: ['follow_request'],
}); });
} }
if (/max_id=($|&)/i.test(notificationsIterator.current?.nextParams)) {
// Pixelfed returns next paginationed link with empty max_id
// I assume, it's done (end of list)
return {
done: true,
};
}
const allNotifications = await notificationsIterator.current.next(); const allNotifications = await notificationsIterator.current.next();
const notifications = allNotifications.value; const notifications = allNotifications.value;
@ -89,21 +82,6 @@ function Notifications({ columnMode }) {
}); });
}); });
// TEST: Slot in a fake notification to test 'severed_relationships'
// notifications.unshift({
// id: '123123',
// type: 'severed_relationships',
// createdAt: '2024-03-22T19:20:08.316Z',
// event: {
// type: 'account_suspension',
// targetName: 'mastodon.dev',
// followersCount: 0,
// followingCount: 0,
// },
// });
// console.log({ notifications });
const groupedNotifications = groupNotifications(notifications); const groupedNotifications = groupNotifications(notifications);
if (firstLoad) { if (firstLoad) {

View file

@ -33,7 +33,6 @@ function Public({ local, columnMode, ...props }) {
publicIterator.current = masto.v1.timelines.public.list({ publicIterator.current = masto.v1.timelines.public.list({
limit: LIMIT, limit: LIMIT,
local: isLocal, local: isLocal,
remote: !isLocal, // Pixelfed
}); });
} }
const results = await publicIterator.current.next(); const results = await publicIterator.current.next();

View file

@ -19,7 +19,6 @@ import pmem from '../utils/pmem';
import shortenNumber from '../utils/shorten-number'; import shortenNumber from '../utils/shorten-number';
import states from '../utils/states'; import states from '../utils/states';
import { saveStatus } from '../utils/states'; import { saveStatus } from '../utils/states';
import supports from '../utils/supports';
import useTitle from '../utils/useTitle'; import useTitle from '../utils/useTitle';
const LIMIT = 20; const LIMIT = 20;
@ -34,17 +33,6 @@ const fetchLinks = pmem(
}, },
); );
function fetchTrends(masto) {
if (supports('@pixelfed/trending')) {
return masto.pixelfed.v2.discover.posts.trending.list({
range: 'daily',
});
}
return masto.v1.trends.statuses.list({
limit: LIMIT,
});
}
function Trending({ columnMode, ...props }) { function Trending({ columnMode, ...props }) {
const snapStates = useSnapshot(states); const snapStates = useSnapshot(states);
const params = columnMode ? {} : useParams(); const params = columnMode ? {} : useParams();
@ -60,39 +48,36 @@ function Trending({ columnMode, ...props }) {
const [hashtags, setHashtags] = useState([]); const [hashtags, setHashtags] = useState([]);
const [links, setLinks] = useState([]); const [links, setLinks] = useState([]);
const trendIterator = useRef(); const trendIterator = useRef();
async function fetchTrend(firstLoad) { async function fetchTrend(firstLoad) {
if (firstLoad || !trendIterator.current) { if (firstLoad || !trendIterator.current) {
trendIterator.current = fetchTrends(masto); trendIterator.current = masto.v1.trends.statuses.list({
limit: LIMIT,
});
// Get hashtags // Get hashtags
if (supports('@mastodon/trending-hashtags')) { try {
try { const iterator = masto.v1.trends.tags.list();
const iterator = masto.v1.trends.tags.list(); const { value: tags } = await iterator.next();
const { value: tags } = await iterator.next(); console.log('tags', tags);
console.log('tags', tags); if (tags?.length) {
if (tags?.length) { setHashtags(tags);
setHashtags(tags);
}
} catch (e) {
console.error(e);
} }
} catch (e) {
console.error(e);
} }
// Get links // Get links
if (supports('@mastodon/trending-links')) { try {
try { const { value } = await fetchLinks(masto, instance);
const { value } = await fetchLinks(masto, instance); // 4 types available: link, photo, video, rich
// 4 types available: link, photo, video, rich // Only want links for now
// Only want links for now const links = value?.filter?.((link) => link.type === 'link');
const links = value?.filter?.((link) => link.type === 'link'); console.log('links', links);
console.log('links', links); if (links?.length) {
if (links?.length) { setLinks(links);
setLinks(links);
}
} catch (e) {
console.error(e);
} }
} catch (e) {
console.error(e);
} }
} }
const results = await trendIterator.current.next(); const results = await trendIterator.current.next();

View file

@ -7,7 +7,6 @@ import {
getAccountByInstance, getAccountByInstance,
getCurrentAccount, getCurrentAccount,
saveAccount, saveAccount,
setCurrentAccountID,
} from './store-utils'; } from './store-utils';
// Default *fallback* instance // Default *fallback* instance
@ -119,7 +118,7 @@ export async function initAccount(client, instance, accessToken, vapidKey) {
const mastoAccount = await masto.v1.accounts.verifyCredentials(); const mastoAccount = await masto.v1.accounts.verifyCredentials();
console.log('CURRENTACCOUNT SET', mastoAccount.id); console.log('CURRENTACCOUNT SET', mastoAccount.id);
setCurrentAccountID(mastoAccount.id); store.session.set('currentAccount', mastoAccount.id);
saveAccount({ saveAccount({
info: mastoAccount, info: mastoAccount,

View file

@ -1,5 +1,5 @@
import mem from './mem'; import mem from './mem';
import { getCurrentAccountID } from './store-utils'; import store from './store';
function _isFiltered(filtered, filterContext) { function _isFiltered(filtered, filterContext) {
if (!filtered?.length) return false; if (!filtered?.length) return false;
@ -43,7 +43,7 @@ export function filteredItem(item, filterContext, currentAccountID) {
export function filteredItems(items, filterContext) { export function filteredItems(items, filterContext) {
if (!items?.length) return []; if (!items?.length) return [];
if (!filterContext) return items; if (!filterContext) return items;
const currentAccountID = getCurrentAccountID(); const currentAccountID = store.session.get('currentAccount');
return items.filter((item) => return items.filter((item) =>
filteredItem(item, filterContext, currentAccountID), filteredItem(item, filterContext, currentAccountID),
); );

View file

@ -9,7 +9,7 @@ const notificationTypeKeys = {
poll: ['status'], poll: ['status'],
update: ['status'], update: ['status'],
}; };
export function fixNotifications(notifications) { function fixNotifications(notifications) {
return notifications.filter((notification) => { return notifications.filter((notification) => {
const { type, id, createdAt } = notification; const { type, id, createdAt } = notification;
if (!type) { if (!type) {

View file

@ -1,11 +1,11 @@
import { api } from './api'; import { api } from './api';
import { getCurrentAccountID } from './store-utils'; import store from './store';
export async function fetchRelationships(accounts, relationshipsMap = {}) { export async function fetchRelationships(accounts, relationshipsMap = {}) {
if (!accounts?.length) return; if (!accounts?.length) return;
const { masto } = api(); const { masto } = api();
const currentAccount = getCurrentAccountID(); const currentAccount = store.session.get('currentAccount');
const uniqueAccountIds = accounts.reduce((acc, a) => { const uniqueAccountIds = accounts.reduce((acc, a) => {
// 1. Ignore duplicate accounts // 1. Ignore duplicate accounts
// 2. Ignore accounts that are already inside relationshipsMap // 2. Ignore accounts that are already inside relationshipsMap

View file

@ -16,40 +16,13 @@ export function getAccountByInstance(instance) {
return accounts.find((a) => a.instanceURL === instance); return accounts.find((a) => a.instanceURL === instance);
} }
const standaloneMQ = window.matchMedia('(display-mode: standalone)');
export function getCurrentAccountID() {
try {
const id = store.session.get('currentAccount');
if (id) return id;
} catch (e) {}
if (standaloneMQ.matches) {
try {
const id = store.local.get('currentAccount');
if (id) return id;
} catch (e) {}
}
return null;
}
export function setCurrentAccountID(id) {
try {
store.session.set('currentAccount', id);
} catch (e) {}
if (standaloneMQ.matches) {
try {
store.local.set('currentAccount', id);
} catch (e) {}
}
}
export function getCurrentAccount() { export function getCurrentAccount() {
if (!window.__IGNORE_GET_ACCOUNT_ERROR__) { if (!window.__IGNORE_GET_ACCOUNT_ERROR__) {
// Track down getCurrentAccount() calls before account-based states are initialized // Track down getCurrentAccount() calls before account-based states are initialized
console.error('getCurrentAccount() called before states are initialized'); console.error('getCurrentAccount() called before states are initialized');
if (import.meta.env.DEV) console.trace(); if (import.meta.env.DEV) console.trace();
} }
const currentAccount = getCurrentAccountID(); const currentAccount = store.session.get('currentAccount');
const account = getAccount(currentAccount); const account = getAccount(currentAccount);
return account; return account;
} }
@ -75,7 +48,7 @@ export function saveAccount(account) {
accounts.push(account); accounts.push(account);
} }
store.local.setJSON('accounts', accounts); store.local.setJSON('accounts', accounts);
setCurrentAccountID(account.info.id); store.session.set('currentAccount', account.info.id);
} }
export function updateAccount(accountInfo) { export function updateAccount(accountInfo) {
@ -107,10 +80,10 @@ export function getCurrentInstance() {
return (currentInstance = instances[instance]); return (currentInstance = instances[instance]);
} catch (e) { } catch (e) {
console.error(e); console.error(e);
// alert(`Failed to load instance configuration. Please try again.\n\n${e}`); alert(`Failed to load instance configuration. Please try again.\n\n${e}`);
// Temporary fix for corrupted data // Temporary fix for corrupted data
// store.local.del('instances'); store.local.del('instances');
// location.reload(); location.reload();
return {}; return {};
} }
} }

View file

@ -4,21 +4,6 @@ import features from '../data/features.json';
import { getCurrentInstance } from './store-utils'; import { getCurrentInstance } from './store-utils';
// Non-semver(?) UA string detection
const containPixelfed = /pixelfed/i;
const notContainPixelfed = /^(?!.*pixelfed).*$/i;
const platformFeatures = {
'@mastodon/lists': notContainPixelfed,
'@mastodon/filters': notContainPixelfed,
'@mastodon/mentions': notContainPixelfed,
'@mastodon/trending-hashtags': notContainPixelfed,
'@mastodon/trending-links': notContainPixelfed,
'@mastodon/post-bookmark': notContainPixelfed,
'@mastodon/post-edit': notContainPixelfed,
'@mastodon/profile-edit': notContainPixelfed,
'@mastodon/profile-private-note': notContainPixelfed,
'@pixelfed/trending': containPixelfed,
};
const supportsCache = {}; const supportsCache = {};
function supports(feature) { function supports(feature) {
@ -26,11 +11,6 @@ function supports(feature) {
const { version, domain } = getCurrentInstance(); const { version, domain } = getCurrentInstance();
const key = `${domain}-${feature}`; const key = `${domain}-${feature}`;
if (supportsCache[key]) return supportsCache[key]; if (supportsCache[key]) return supportsCache[key];
if (platformFeatures[feature]) {
return (supportsCache[key] = platformFeatures[feature].test(version));
}
const range = features[feature]; const range = features[feature];
if (!range) return false; if (!range) return false;
return (supportsCache[key] = satisfies(version, range, { return (supportsCache[key] = satisfies(version, range, {