mirror of
https://github.com/cheeaun/phanpy.git
synced 2025-01-22 16:46:28 +01:00
Initial i18n dev
Expecting bugs!
This commit is contained in:
parent
3f23fe6eb6
commit
c2e6d732c4
76 changed files with 13355 additions and 2042 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -27,3 +27,6 @@ dist-ssr
|
||||||
.env.dev
|
.env.dev
|
||||||
phanpy-dist.zip
|
phanpy-dist.zip
|
||||||
phanpy-dist.tar.gz
|
phanpy-dist.tar.gz
|
||||||
|
|
||||||
|
# Compiled locale files
|
||||||
|
src/locales/*.js
|
62
README.md
62
README.md
|
@ -100,11 +100,13 @@ Everything is designed and engineered following my taste and vision. This is a p
|
||||||
Prerequisites: Node.js 18+
|
Prerequisites: Node.js 18+
|
||||||
|
|
||||||
- `npm install` - Install dependencies
|
- `npm install` - Install dependencies
|
||||||
- `npm run dev` - Start development server
|
- `npm run dev` - Start development server and `messages:extract:watch` in parallel
|
||||||
- `npm run build` - Build for production
|
- `npm run build` - Build for production
|
||||||
- `npm run preview` - Preview the production build
|
- `npm run preview` - Preview the production build
|
||||||
- `npm run fetch-instances` - Fetch instances list from [joinmastodon.org/servers](https://joinmastodon.org/servers), save it to `src/data/instances.json`
|
- `npm run fetch-instances` - Fetch instances list from [joinmastodon.org/servers](https://joinmastodon.org/servers), save it to `src/data/instances.json`
|
||||||
- `npm run sourcemap` - Run `source-map-explorer` on the production build
|
- `npm run sourcemap` - Run `source-map-explorer` on the production build
|
||||||
|
- `npm run messages:extract` - Extract messages from source files and update the locale message catalogs
|
||||||
|
- `npm run messages:extract:watch` - Same as `messages:extract` but in watch mode
|
||||||
|
|
||||||
## Tech stack
|
## Tech stack
|
||||||
|
|
||||||
|
@ -115,10 +117,65 @@ Prerequisites: Node.js 18+
|
||||||
- [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/)
|
- [MingCute icons](https://www.mingcute.com/)
|
||||||
|
- [Lingui](https://lingui.dev/) - Internationalization
|
||||||
- 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.
|
||||||
|
|
||||||
|
## Internationalization
|
||||||
|
|
||||||
|
All translations are available as [gettext](https://en.wikipedia.org/wiki/Gettext) `.po` files in the `src/locales` folder. The default language is English (`en`). [CLDR Plural Rules](https://cldr.unicode.org/index/cldr-spec/plural-rules) are used for pluralization. RTL (right-to-left) languages are also supported with proper text direction, icon rendering and layout.
|
||||||
|
|
||||||
|
On page load, default language is detected via these methods, in order (first match is used):
|
||||||
|
|
||||||
|
1. URL parameter `lang` e.g. `/?lang=zh-Hant`
|
||||||
|
2. `localStorage` key `lang`
|
||||||
|
3. Browser's `navigator.language`
|
||||||
|
|
||||||
|
Users can change the language in the settings, which sets the `localStorage` key `lang`.
|
||||||
|
|
||||||
|
### Guide for translators
|
||||||
|
|
||||||
|
*Inspired by [Translate WordPress Handbook](https://make.wordpress.org/polyglots/handbook/):
|
||||||
|
|
||||||
|
- [Don’t translate literally, translate organically](https://make.wordpress.org/polyglots/handbook/translating/expectations/#dont-translate-literally-translate-organically).
|
||||||
|
- [Try to keep the same level of formality (or informality)](https://make.wordpress.org/polyglots/handbook/translating/expectations/#try-to-keep-the-same-level-of-formality-or-informality)
|
||||||
|
- [Don’t use slang or audience-specific terms](https://make.wordpress.org/polyglots/handbook/translating/expectations/#try-to-keep-the-same-level-of-formality-or-informality)
|
||||||
|
- Be attentive to placeholders for variables. Many strings have placesholders e.g. `{account}` (variable), `<0>{name}</0>` (tag with variable) and `#` (number placeholder).
|
||||||
|
- [Ellipsis](https://en.wikipedia.org/wiki/Ellipsis) (…) is intentional. Don't remove it.
|
||||||
|
- Nielsen Norman Group: ["Include Ellipses in Command Text to Indicate When More Information Is Required"](https://www.nngroup.com/articles/ui-copy/)
|
||||||
|
- Apple Human Interface Guidelines: ["Append an ellipsis to a menu item’s label when the action requires more information before it can complete. The ellipsis character (…) signals that people need to input information or make additional choices, typically within another view."](https://developer.apple.com/design/human-interface-guidelines/menus)
|
||||||
|
- Windows App Development: ["Ellipses mean incompleteness."](https://learn.microsoft.com/en-us/windows/win32/uxguide/text-ui)
|
||||||
|
- Date timestamps, date ranges, numbers, language names and text segmentation are handled by the [ECMAScript Internationalization API](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl).
|
||||||
|
- [`Intl.DateTimeFormat`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat) - e.g. "8 Aug", "08/08/2024"
|
||||||
|
- [`Intl.RelativeTimeFormat`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/RelativeTimeFormat) - e.g. "2 days ago", "in 2 days"
|
||||||
|
- [`Intl.NumberFormat`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat) - e.g. "1,000", "10K"
|
||||||
|
- [`Intl.DisplayNames`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DisplayNames) - e.g. "English" (`en`) in Traditional Chinese (`zh-Hant`) is "英文"
|
||||||
|
- [`Intl.Locale`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale) (with polyfill for older browsers)
|
||||||
|
- [`Intl.Segmenter`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Segmenter) (with polyfill for older browsers)
|
||||||
|
|
||||||
|
### Technical notes
|
||||||
|
|
||||||
|
- IDs for strings are auto-generated instead of explicitly defined. Some of the [benefits](https://lingui.dev/tutorials/explicit-vs-generated-ids#benefits-of-generated-ids) are avoiding the "naming things" problem and avoiding duplicates.
|
||||||
|
- Explicit IDs might be introduced in the future when requirements and priorities change. The library (Lingui) allows both.
|
||||||
|
- Please report issues if certain strings are translated differently based on context, culture or region.
|
||||||
|
- There are no strings for push notifications. The language is set on the instance server.
|
||||||
|
- Native HTML date pickers, e.g. `<input type="month">` will always follow the system's locale and not the user's set locale.
|
||||||
|
- "ALT" in ALT badge is not translated. It serves as a a recognizable standard across languages.
|
||||||
|
- Custom emoji names are not localized, therefore searches don't work for non-English languages.
|
||||||
|
- GIPHY API supports [a list of languages for searches](https://developers.giphy.com/docs/optional-settings/#language-support).
|
||||||
|
- Unicode Right-to-left mark (RLM) (`U+200F`, `‏`) may need to be used for mixed RTL/LTR text, especially for [`<title>` element](https://www.w3.org/International/questions/qa-html-dir.en.html#title_element) (`document.title`).
|
||||||
|
- On development, there's an additional `pseudo-LOCALE` locale, used for [pseudolocalization](https://en.wikipedia.org/wiki/Pseudolocalization). It's for testing and won't show up on production.
|
||||||
|
- When building for production, English (`en`) catalog messages are not bundled separatedly. Other locales are bundled as separate files and loaded on demand. This ensures that `en` is always available as fallback.
|
||||||
|
|
||||||
|
### Volunteer translations
|
||||||
|
|
||||||
|
[![Crowdin](https://badges.crowdin.net/phanpy/localized.svg)](https://crowdin.com/project/phanpy)
|
||||||
|
|
||||||
|
Translations are managed on [Crowdin](https://crowdin.com/project/phanpy). You can help by volunteering translations.
|
||||||
|
|
||||||
|
Read the [intro documentation](https://support.crowdin.com/for-volunteer-translators/) to get started.
|
||||||
|
|
||||||
## Self-hosting
|
## Self-hosting
|
||||||
|
|
||||||
This is a **pure static web app**. You can host it anywhere you want.
|
This is a **pure static web app**. You can host it anywhere you want.
|
||||||
|
@ -174,6 +231,9 @@ Available variables:
|
||||||
- `PHANPY_PRIVACY_POLICY_URL` (optional, default to official instance's privacy policy):
|
- `PHANPY_PRIVACY_POLICY_URL` (optional, default to official instance's privacy policy):
|
||||||
- URL of the privacy policy page
|
- URL of the privacy policy page
|
||||||
- May specify the instance's own privacy policy
|
- May specify the instance's own privacy policy
|
||||||
|
- `PHANPY_DEFAULT_LANG` (optional):
|
||||||
|
- Default language is English (`en`) if not specified.
|
||||||
|
- Fallback language after multiple detection methods (`lang` query parameter, `lang` key in `localStorage` and `navigator.language`)
|
||||||
- `PHANPY_LINGVA_INSTANCES` (optional, space-separated list, default: `lingva.phanpy.social [...hard-coded list of fallback instances]`):
|
- `PHANPY_LINGVA_INSTANCES` (optional, space-separated list, default: `lingva.phanpy.social [...hard-coded list of fallback instances]`):
|
||||||
- Specify a space-separated list of instances. First will be used as default before falling back to the subsequent instances. If there's only 1 instance, means no fallback.
|
- Specify a space-separated list of instances. First will be used as default before falling back to the subsequent instances. If there's only 1 instance, means no fallback.
|
||||||
- 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)
|
||||||
|
|
17
lingui.config.js
Normal file
17
lingui.config.js
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
const config = {
|
||||||
|
locales: ['en', 'pseudo-LOCALE'],
|
||||||
|
pseudoLocale: 'pseudo-LOCALE',
|
||||||
|
fallbackLocales: {
|
||||||
|
default: 'en',
|
||||||
|
},
|
||||||
|
catalogs: [
|
||||||
|
{
|
||||||
|
path: '<rootDir>/src/locales/{locale}',
|
||||||
|
include: ['src'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
compileNamespace: 'es',
|
||||||
|
orderBy: 'origin',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
2541
package-lock.json
generated
2541
package-lock.json
generated
File diff suppressed because it is too large
Load diff
18
package.json
18
package.json
|
@ -3,12 +3,17 @@
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev:vite": "vite",
|
||||||
|
"dev": "run-p dev:vite messages:extract:watch",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"fetch-instances": "env $(cat .env.local | grep -v \"#\" | xargs) node scripts/fetch-instances-list.js",
|
"fetch-instances": "env $(cat .env.local | grep -v \"#\" | xargs) node scripts/fetch-instances-list.js",
|
||||||
"sourcemap": "npx source-map-explorer dist/assets/*.js",
|
"sourcemap": "npx source-map-explorer dist/assets/*.js",
|
||||||
"bundle-visualizer": "npx vite-bundle-visualizer"
|
"bundle-visualizer": "npx vite-bundle-visualizer",
|
||||||
|
"messages:extract": "lingui extract",
|
||||||
|
"messages:extract:watch": "lingui extract --watch",
|
||||||
|
"messages:extract:clean": "lingui extract --clean",
|
||||||
|
"messages:compile": "lingui compile"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@formatjs/intl-localematcher": "~0.5.4",
|
"@formatjs/intl-localematcher": "~0.5.4",
|
||||||
|
@ -17,15 +22,18 @@
|
||||||
"@github/text-expander-element": "~2.7.1",
|
"@github/text-expander-element": "~2.7.1",
|
||||||
"@iconify-icons/mingcute": "~1.2.9",
|
"@iconify-icons/mingcute": "~1.2.9",
|
||||||
"@justinribeiro/lite-youtube": "~1.5.0",
|
"@justinribeiro/lite-youtube": "~1.5.0",
|
||||||
|
"@lingui/detect-locale": "~4.11.3",
|
||||||
|
"@lingui/macro": "~4.11.3",
|
||||||
|
"@lingui/react": "~4.11.3",
|
||||||
"@szhsin/react-menu": "~4.2.1",
|
"@szhsin/react-menu": "~4.2.1",
|
||||||
"compare-versions": "~6.1.1",
|
"compare-versions": "~6.1.1",
|
||||||
"dayjs": "~1.11.12",
|
"dayjs": "~1.11.12",
|
||||||
"dayjs-twitter": "~0.5.0",
|
|
||||||
"fast-blurhash": "~1.1.4",
|
"fast-blurhash": "~1.1.4",
|
||||||
"fast-equals": "~5.0.1",
|
"fast-equals": "~5.0.1",
|
||||||
"fuse.js": "~7.0.0",
|
"fuse.js": "~7.0.0",
|
||||||
"html-prettify": "~1.0.7",
|
"html-prettify": "~1.0.7",
|
||||||
"idb-keyval": "~6.2.1",
|
"idb-keyval": "~6.2.1",
|
||||||
|
"intl-locale-textinfo-polyfill": "~2.1.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.8.0",
|
"masto": "~6.8.0",
|
||||||
|
@ -50,7 +58,11 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@ianvs/prettier-plugin-sort-imports": "~4.3.1",
|
"@ianvs/prettier-plugin-sort-imports": "~4.3.1",
|
||||||
|
"@lingui/cli": "~4.11.3",
|
||||||
|
"@lingui/vite-plugin": "~4.11.3",
|
||||||
"@preact/preset-vite": "~2.9.0",
|
"@preact/preset-vite": "~2.9.0",
|
||||||
|
"babel-plugin-macros": "~3.1.0",
|
||||||
|
"npm-run-all2": "~6.2.2",
|
||||||
"postcss": "~8.4.40",
|
"postcss": "~8.4.40",
|
||||||
"postcss-dark-theme-class": "~1.3.0",
|
"postcss-dark-theme-class": "~1.3.0",
|
||||||
"postcss-preset-env": "~10.0.0",
|
"postcss-preset-env": "~10.0.0",
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import './app.css';
|
import './app.css';
|
||||||
|
|
||||||
|
import { useLingui } from '@lingui/react';
|
||||||
import debounce from 'just-debounce-it';
|
import debounce from 'just-debounce-it';
|
||||||
import {
|
import {
|
||||||
useEffect,
|
useEffect,
|
||||||
|
@ -299,6 +300,7 @@ subscribe(states, (changes) => {
|
||||||
function App() {
|
function App() {
|
||||||
const [isLoggedIn, setIsLoggedIn] = useState(false);
|
const [isLoggedIn, setIsLoggedIn] = useState(false);
|
||||||
const [uiState, setUIState] = useState('loading');
|
const [uiState, setUIState] = useState('loading');
|
||||||
|
useLingui();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const instanceURL = store.local.get('instanceURL');
|
const instanceURL = store.local.get('instanceURL');
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import './account-block.css';
|
import './account-block.css';
|
||||||
|
|
||||||
|
import { Plural, t, Trans } from '@lingui/macro';
|
||||||
|
|
||||||
// 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';
|
||||||
|
@ -128,20 +130,23 @@ function AccountBlock({
|
||||||
{locked && (
|
{locked && (
|
||||||
<>
|
<>
|
||||||
{' '}
|
{' '}
|
||||||
<Icon icon="lock" size="s" alt="Locked" />
|
<Icon icon="lock" size="s" alt={t`Locked`} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
{showActivity && (
|
{showActivity && (
|
||||||
<div class="account-block-stats">
|
<div class="account-block-stats">
|
||||||
Posts: {shortenNumber(statusesCount)}
|
<Trans>Posts: {shortenNumber(statusesCount)}</Trans>
|
||||||
{!!lastStatusAt && (
|
{!!lastStatusAt && (
|
||||||
<>
|
<>
|
||||||
{' '}
|
{' '}
|
||||||
· Last posted:{' '}
|
·{' '}
|
||||||
|
<Trans>
|
||||||
|
Last posted:{' '}
|
||||||
{niceDateTime(lastStatusAt, {
|
{niceDateTime(lastStatusAt, {
|
||||||
hideTime: true,
|
hideTime: true,
|
||||||
})}
|
})}
|
||||||
|
</Trans>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -151,14 +156,14 @@ function AccountBlock({
|
||||||
{bot && (
|
{bot && (
|
||||||
<>
|
<>
|
||||||
<span class="tag collapsed">
|
<span class="tag collapsed">
|
||||||
<Icon icon="bot" /> Automated
|
<Icon icon="bot" /> <Trans>Automated</Trans>
|
||||||
</span>
|
</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{!!group && (
|
{!!group && (
|
||||||
<>
|
<>
|
||||||
<span class="tag collapsed">
|
<span class="tag collapsed">
|
||||||
<Icon icon="group" /> Group
|
<Icon icon="group" /> <Trans>Group</Trans>
|
||||||
</span>
|
</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
@ -167,26 +172,37 @@ function AccountBlock({
|
||||||
<div class="shazam-container-inner">
|
<div class="shazam-container-inner">
|
||||||
{excludedRelationship.following &&
|
{excludedRelationship.following &&
|
||||||
excludedRelationship.followedBy ? (
|
excludedRelationship.followedBy ? (
|
||||||
<span class="tag minimal">Mutual</span>
|
<span class="tag minimal">
|
||||||
|
<Trans>Mutual</Trans>
|
||||||
|
</span>
|
||||||
) : excludedRelationship.requested ? (
|
) : excludedRelationship.requested ? (
|
||||||
<span class="tag minimal">Requested</span>
|
<span class="tag minimal">
|
||||||
|
<Trans>Requested</Trans>
|
||||||
|
</span>
|
||||||
) : excludedRelationship.following ? (
|
) : excludedRelationship.following ? (
|
||||||
<span class="tag minimal">Following</span>
|
<span class="tag minimal">
|
||||||
|
<Trans>Following</Trans>
|
||||||
|
</span>
|
||||||
) : excludedRelationship.followedBy ? (
|
) : excludedRelationship.followedBy ? (
|
||||||
<span class="tag minimal">Follows you</span>
|
<span class="tag minimal">
|
||||||
|
<Trans>Follows you</Trans>
|
||||||
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!!followersCount && (
|
{!!followersCount && (
|
||||||
<span class="ib">
|
<span class="ib">
|
||||||
{shortenNumber(followersCount)}{' '}
|
<Plural
|
||||||
{followersCount === 1 ? 'follower' : 'followers'}
|
value={followersCount}
|
||||||
|
one="# follower"
|
||||||
|
other="# followers"
|
||||||
|
/>
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{!!verifiedField && (
|
{!!verifiedField && (
|
||||||
<span class="verified-field">
|
<span class="verified-field">
|
||||||
<Icon icon="check-circle" size="s" />{' '}
|
<Icon icon="check-circle" size="s" alt={t`Verified`} />{' '}
|
||||||
<span
|
<span
|
||||||
dangerouslySetInnerHTML={{
|
dangerouslySetInnerHTML={{
|
||||||
__html: enhanceContent(verifiedField.value, { emojis }),
|
__html: enhanceContent(verifiedField.value, { emojis }),
|
||||||
|
@ -201,12 +217,14 @@ function AccountBlock({
|
||||||
!verifiedField &&
|
!verifiedField &&
|
||||||
!!createdAt && (
|
!!createdAt && (
|
||||||
<span class="created-at">
|
<span class="created-at">
|
||||||
|
<Trans>
|
||||||
Joined{' '}
|
Joined{' '}
|
||||||
<time datetime={createdAt}>
|
<time datetime={createdAt}>
|
||||||
{niceDateTime(createdAt, {
|
{niceDateTime(createdAt, {
|
||||||
hideTime: true,
|
hideTime: true,
|
||||||
})}
|
})}
|
||||||
</time>
|
</time>
|
||||||
|
</Trans>
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import './account-info.css';
|
import './account-info.css';
|
||||||
|
|
||||||
|
import { msg, plural, t, Trans } from '@lingui/macro';
|
||||||
|
import { useLingui } from '@lingui/react';
|
||||||
import { MenuDivider, MenuItem } from '@szhsin/react-menu';
|
import { MenuDivider, MenuItem } from '@szhsin/react-menu';
|
||||||
import {
|
import {
|
||||||
useCallback,
|
useCallback,
|
||||||
|
@ -15,6 +17,7 @@ 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 i18nDuration from '../utils/i18n-duration';
|
||||||
import { getLists } from '../utils/lists';
|
import { getLists } from '../utils/lists';
|
||||||
import niceDateTime from '../utils/nice-date-time';
|
import niceDateTime from '../utils/nice-date-time';
|
||||||
import pmem from '../utils/pmem';
|
import pmem from '../utils/pmem';
|
||||||
|
@ -51,15 +54,16 @@ const MUTE_DURATIONS = [
|
||||||
0, // forever
|
0, // forever
|
||||||
];
|
];
|
||||||
const MUTE_DURATIONS_LABELS = {
|
const MUTE_DURATIONS_LABELS = {
|
||||||
0: 'Forever',
|
0: msg`Forever`,
|
||||||
300: '5 minutes',
|
300: i18nDuration(5, 'minute'),
|
||||||
1_800: '30 minutes',
|
1_800: i18nDuration(30, 'minute'),
|
||||||
3_600: '1 hour',
|
3_600: i18nDuration(1, 'hour'),
|
||||||
21_600: '6 hours',
|
21_600: i18nDuration(6, 'hour'),
|
||||||
86_400: '1 day',
|
86_400: i18nDuration(1, 'day'),
|
||||||
259_200: '3 days',
|
259_200: i18nDuration(3, 'day'),
|
||||||
604_800: '1 week',
|
604_800: i18nDuration(1, 'week'),
|
||||||
};
|
};
|
||||||
|
console.log({ MUTE_DURATIONS_LABELS });
|
||||||
|
|
||||||
const LIMIT = 80;
|
const LIMIT = 80;
|
||||||
|
|
||||||
|
@ -130,6 +134,7 @@ function AccountInfo({
|
||||||
instance,
|
instance,
|
||||||
authenticated,
|
authenticated,
|
||||||
}) {
|
}) {
|
||||||
|
const { i18n } = useLingui();
|
||||||
const { masto } = api({
|
const { masto } = api({
|
||||||
instance,
|
instance,
|
||||||
});
|
});
|
||||||
|
@ -369,14 +374,16 @@ function AccountInfo({
|
||||||
>
|
>
|
||||||
{uiState === 'error' && (
|
{uiState === 'error' && (
|
||||||
<div class="ui-state">
|
<div class="ui-state">
|
||||||
<p>Unable to load account.</p>
|
<p>
|
||||||
|
<Trans>Unable to load account.</Trans>
|
||||||
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<a
|
<a
|
||||||
href={isString ? account : url}
|
href={isString ? account : url}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
>
|
>
|
||||||
Go to account page <Icon icon="external" />
|
<Trans>Go to account page</Trans> <Icon icon="external" />
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -404,21 +411,21 @@ function AccountInfo({
|
||||||
</div>
|
</div>
|
||||||
<div class="stats">
|
<div class="stats">
|
||||||
<div>
|
<div>
|
||||||
<span>██</span> Followers
|
<span>██</span> <Trans>Followers</Trans>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span>██</span> Following
|
<span>██</span> <Trans>Following</Trans>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span>██</span> Posts
|
<span>██</span> <Trans>Posts</Trans>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<span />
|
<span />
|
||||||
<span class="buttons">
|
<span class="buttons">
|
||||||
<button type="button" title="More" class="plain" disabled>
|
<button type="button" class="plain" disabled>
|
||||||
<Icon icon="more" size="l" alt="More" />
|
<Icon icon="more" size="l" alt={t`More`} />
|
||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
@ -430,8 +437,10 @@ function AccountInfo({
|
||||||
{!!moved && (
|
{!!moved && (
|
||||||
<div class="account-moved">
|
<div class="account-moved">
|
||||||
<p>
|
<p>
|
||||||
|
<Trans>
|
||||||
<b>{displayName}</b> has indicated that their new account is
|
<b>{displayName}</b> has indicated that their new account is
|
||||||
now:
|
now:
|
||||||
|
</Trans>
|
||||||
</p>
|
</p>
|
||||||
<AccountBlock
|
<AccountBlock
|
||||||
account={moved}
|
account={moved}
|
||||||
|
@ -573,28 +582,36 @@ function AccountInfo({
|
||||||
: `@${acct}@${instance}`;
|
: `@${acct}@${instance}`;
|
||||||
try {
|
try {
|
||||||
navigator.clipboard.writeText(handleWithInstance);
|
navigator.clipboard.writeText(handleWithInstance);
|
||||||
showToast('Handle copied');
|
showToast(t`Handle copied`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
showToast('Unable to copy handle');
|
showToast(t`Unable to copy handle`);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon icon="link" />
|
<Icon icon="link" />
|
||||||
<span>Copy handle</span>
|
<span>
|
||||||
|
<Trans>Copy handle</Trans>
|
||||||
|
</span>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem href={url} target="_blank">
|
<MenuItem href={url} target="_blank">
|
||||||
<Icon icon="external" />
|
<Icon icon="external" />
|
||||||
<span>Go to original profile page</span>
|
<span>
|
||||||
|
<Trans>Go to original profile page</Trans>
|
||||||
|
</span>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuDivider />
|
<MenuDivider />
|
||||||
<MenuLink href={info.avatar} target="_blank">
|
<MenuLink href={info.avatar} target="_blank">
|
||||||
<Icon icon="user" />
|
<Icon icon="user" />
|
||||||
<span>View profile image</span>
|
<span>
|
||||||
|
<Trans>View profile image</Trans>
|
||||||
|
</span>
|
||||||
</MenuLink>
|
</MenuLink>
|
||||||
<MenuLink href={info.header} target="_blank">
|
<MenuLink href={info.header} target="_blank">
|
||||||
<Icon icon="media" />
|
<Icon icon="media" />
|
||||||
<span>View profile header</span>
|
<span>
|
||||||
|
<Trans>View profile header</Trans>
|
||||||
|
</span>
|
||||||
</MenuLink>
|
</MenuLink>
|
||||||
</Menu2>
|
</Menu2>
|
||||||
) : (
|
) : (
|
||||||
|
@ -608,15 +625,19 @@ function AccountInfo({
|
||||||
</header>
|
</header>
|
||||||
<div class="faux-header-bg" aria-hidden="true" />
|
<div class="faux-header-bg" aria-hidden="true" />
|
||||||
<main>
|
<main>
|
||||||
{!!memorial && <span class="tag">In Memoriam</span>}
|
{!!memorial && (
|
||||||
|
<span class="tag">
|
||||||
|
<Trans>In Memoriam</Trans>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
{!!bot && (
|
{!!bot && (
|
||||||
<span class="tag">
|
<span class="tag">
|
||||||
<Icon icon="bot" /> Automated
|
<Icon icon="bot" /> <Trans>Automated</Trans>
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{!!group && (
|
{!!group && (
|
||||||
<span class="tag">
|
<span class="tag">
|
||||||
<Icon icon="group" /> Group
|
<Icon icon="group" /> <Trans>Group</Trans>
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{roles?.map((role) => (
|
{roles?.map((role) => (
|
||||||
|
@ -654,7 +675,11 @@ function AccountInfo({
|
||||||
<b>
|
<b>
|
||||||
<EmojiText text={name} emojis={emojis} />{' '}
|
<EmojiText text={name} emojis={emojis} />{' '}
|
||||||
{!!verifiedAt && (
|
{!!verifiedAt && (
|
||||||
<Icon icon="check-circle" size="s" />
|
<Icon
|
||||||
|
icon="check-circle"
|
||||||
|
size="s"
|
||||||
|
alt={t`Verified`}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</b>
|
</b>
|
||||||
<p
|
<p
|
||||||
|
@ -675,14 +700,14 @@ function AccountInfo({
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
states.showGenericAccounts = {
|
states.showGenericAccounts = {
|
||||||
id: 'followers',
|
id: 'followers',
|
||||||
heading: 'Followers',
|
heading: t`Followers`,
|
||||||
fetchAccounts: fetchFollowers,
|
fetchAccounts: fetchFollowers,
|
||||||
instance,
|
instance,
|
||||||
excludeRelationshipAttrs: isSelf
|
excludeRelationshipAttrs: isSelf
|
||||||
? ['followedBy']
|
? ['followedBy']
|
||||||
: [],
|
: [],
|
||||||
blankCopy: hideCollections
|
blankCopy: hideCollections
|
||||||
? 'This user has chosen to not make this information available.'
|
? t`This user has chosen to not make this information available.`
|
||||||
: undefined,
|
: undefined,
|
||||||
};
|
};
|
||||||
}, 0);
|
}, 0);
|
||||||
|
@ -705,7 +730,7 @@ function AccountInfo({
|
||||||
<span title={followersCount}>
|
<span title={followersCount}>
|
||||||
{shortenNumber(followersCount)}
|
{shortenNumber(followersCount)}
|
||||||
</span>{' '}
|
</span>{' '}
|
||||||
Followers
|
<Trans>Followers</Trans>
|
||||||
</LinkOrDiv>
|
</LinkOrDiv>
|
||||||
<LinkOrDiv
|
<LinkOrDiv
|
||||||
class="insignificant"
|
class="insignificant"
|
||||||
|
@ -715,12 +740,12 @@ function AccountInfo({
|
||||||
// states.showAccount = false;
|
// states.showAccount = false;
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
states.showGenericAccounts = {
|
states.showGenericAccounts = {
|
||||||
heading: 'Following',
|
heading: t`Following`,
|
||||||
fetchAccounts: fetchFollowing,
|
fetchAccounts: fetchFollowing,
|
||||||
instance,
|
instance,
|
||||||
excludeRelationshipAttrs: isSelf ? ['following'] : [],
|
excludeRelationshipAttrs: isSelf ? ['following'] : [],
|
||||||
blankCopy: hideCollections
|
blankCopy: hideCollections
|
||||||
? 'This user has chosen to not make this information available.'
|
? t`This user has chosen to not make this information available.`
|
||||||
: undefined,
|
: undefined,
|
||||||
};
|
};
|
||||||
}, 0);
|
}, 0);
|
||||||
|
@ -729,7 +754,7 @@ function AccountInfo({
|
||||||
<span title={followingCount}>
|
<span title={followingCount}>
|
||||||
{shortenNumber(followingCount)}
|
{shortenNumber(followingCount)}
|
||||||
</span>{' '}
|
</span>{' '}
|
||||||
Following
|
<Trans>Following</Trans>
|
||||||
<br />
|
<br />
|
||||||
</LinkOrDiv>
|
</LinkOrDiv>
|
||||||
<LinkOrDiv
|
<LinkOrDiv
|
||||||
|
@ -746,16 +771,18 @@ function AccountInfo({
|
||||||
<span title={statusesCount}>
|
<span title={statusesCount}>
|
||||||
{shortenNumber(statusesCount)}
|
{shortenNumber(statusesCount)}
|
||||||
</span>{' '}
|
</span>{' '}
|
||||||
Posts
|
<Trans>Posts</Trans>
|
||||||
</LinkOrDiv>
|
</LinkOrDiv>
|
||||||
{!!createdAt && (
|
{!!createdAt && (
|
||||||
<div class="insignificant">
|
<div class="insignificant">
|
||||||
|
<Trans>
|
||||||
Joined{' '}
|
Joined{' '}
|
||||||
<time datetime={createdAt}>
|
<time datetime={createdAt}>
|
||||||
{niceDateTime(createdAt, {
|
{niceDateTime(createdAt, {
|
||||||
hideTime: true,
|
hideTime: true,
|
||||||
})}
|
})}
|
||||||
</time>
|
</time>
|
||||||
|
</Trans>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -773,25 +800,39 @@ function AccountInfo({
|
||||||
{hasPostingStats ? (
|
{hasPostingStats ? (
|
||||||
<div
|
<div
|
||||||
class="posting-stats"
|
class="posting-stats"
|
||||||
title={`${Math.round(
|
title={t`${(
|
||||||
(postingStats.originals / postingStats.total) * 100,
|
postingStats.originals / postingStats.total
|
||||||
)}% original posts, ${Math.round(
|
).toLocaleString(i18n.locale || undefined, {
|
||||||
(postingStats.replies / postingStats.total) * 100,
|
style: 'percent',
|
||||||
)}% replies, ${Math.round(
|
})} original posts, ${(
|
||||||
(postingStats.boosts / postingStats.total) * 100,
|
postingStats.replies / postingStats.total
|
||||||
)}% boosts`}
|
).toLocaleString(i18n.locale || undefined, {
|
||||||
|
style: 'percent',
|
||||||
|
})} replies, ${(
|
||||||
|
postingStats.boosts / postingStats.total
|
||||||
|
).toLocaleString(i18n.locale || undefined, {
|
||||||
|
style: 'percent',
|
||||||
|
})} boosts`}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
{postingStats.daysSinceLastPost < 365
|
{postingStats.daysSinceLastPost < 365
|
||||||
? `Last ${postingStats.total} post${
|
? plural(postingStats.total, {
|
||||||
postingStats.total > 1 ? 's' : ''
|
one: plural(postingStats.daysSinceLastPost, {
|
||||||
} in the past
|
one: `Last 1 post in the past 1 day`,
|
||||||
${postingStats.daysSinceLastPost} day${
|
other: `Last 1 post in the past ${postingStats.daysSinceLastPost} days`,
|
||||||
postingStats.daysSinceLastPost > 1 ? 's' : ''
|
}),
|
||||||
}`
|
other: plural(
|
||||||
: `
|
postingStats.daysSinceLastPost,
|
||||||
Last ${postingStats.total} posts in the past year(s)
|
{
|
||||||
`}
|
one: `Last ${postingStats.total} posts in the past 1 day`,
|
||||||
|
other: `Last ${postingStats.total} posts in the past ${postingStats.daysSinceLastPost} days`,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
})
|
||||||
|
: plural(postingStats.total, {
|
||||||
|
one: 'Last 1 post in the past year(s)',
|
||||||
|
other: `Last ${postingStats.total} posts in the past year(s)`,
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="posting-stats-bar"
|
class="posting-stats-bar"
|
||||||
|
@ -812,20 +853,22 @@ function AccountInfo({
|
||||||
<div class="posting-stats-legends">
|
<div class="posting-stats-legends">
|
||||||
<span class="ib">
|
<span class="ib">
|
||||||
<span class="posting-stats-legend-item posting-stats-legend-item-originals" />{' '}
|
<span class="posting-stats-legend-item posting-stats-legend-item-originals" />{' '}
|
||||||
Original
|
<Trans>Original</Trans>
|
||||||
</span>{' '}
|
</span>{' '}
|
||||||
<span class="ib">
|
<span class="ib">
|
||||||
<span class="posting-stats-legend-item posting-stats-legend-item-replies" />{' '}
|
<span class="posting-stats-legend-item posting-stats-legend-item-replies" />{' '}
|
||||||
Replies
|
<Trans>Replies</Trans>
|
||||||
</span>{' '}
|
</span>{' '}
|
||||||
<span class="ib">
|
<span class="ib">
|
||||||
<span class="posting-stats-legend-item posting-stats-legend-item-boosts" />{' '}
|
<span class="posting-stats-legend-item posting-stats-legend-item-boosts" />{' '}
|
||||||
Boosts
|
<Trans>Boosts</Trans>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div class="posting-stats">Post stats unavailable.</div>
|
<div class="posting-stats">
|
||||||
|
<Trans>Post stats unavailable.</Trans>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -855,7 +898,7 @@ function AccountInfo({
|
||||||
'--replies-percentage': '66%',
|
'--replies-percentage': '66%',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
View post stats{' '}
|
<Trans>View post stats</Trans>{' '}
|
||||||
{/* <Loader
|
{/* <Loader
|
||||||
abrupt
|
abrupt
|
||||||
hidden={postingStatsUIState !== 'loading'}
|
hidden={postingStatsUIState !== 'loading'}
|
||||||
|
@ -894,6 +937,7 @@ function RelatedActions({
|
||||||
onProfileUpdate = () => {},
|
onProfileUpdate = () => {},
|
||||||
}) {
|
}) {
|
||||||
if (!info) return null;
|
if (!info) return null;
|
||||||
|
const { _ } = useLingui();
|
||||||
const {
|
const {
|
||||||
masto: currentMasto,
|
masto: currentMasto,
|
||||||
instance: currentInstance,
|
instance: currentInstance,
|
||||||
|
@ -1012,28 +1056,40 @@ function RelatedActions({
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<span>
|
<span>
|
||||||
{followedBy ? (
|
{followedBy ? (
|
||||||
<span class="tag">Follows you</span>
|
<span class="tag">
|
||||||
|
<Trans>Follows you</Trans>
|
||||||
|
</span>
|
||||||
) : !!lastStatusAt ? (
|
) : !!lastStatusAt ? (
|
||||||
<small class="insignificant">
|
<small class="insignificant">
|
||||||
|
<Trans>
|
||||||
Last post:{' '}
|
Last post:{' '}
|
||||||
<span class="ib">
|
<span class="ib">
|
||||||
{niceDateTime(lastStatusAt, {
|
{niceDateTime(lastStatusAt, {
|
||||||
hideTime: true,
|
hideTime: true,
|
||||||
})}
|
})}
|
||||||
</span>
|
</span>
|
||||||
|
</Trans>
|
||||||
</small>
|
</small>
|
||||||
) : (
|
) : (
|
||||||
<span />
|
<span />
|
||||||
)}
|
)}
|
||||||
{muting && <span class="tag danger">Muted</span>}
|
{muting && (
|
||||||
{blocking && <span class="tag danger">Blocked</span>}
|
<span class="tag danger">
|
||||||
|
<Trans>Muted</Trans>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{blocking && (
|
||||||
|
<span class="tag danger">
|
||||||
|
<Trans>Blocked</Trans>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</span>{' '}
|
</span>{' '}
|
||||||
<span class="buttons">
|
<span class="buttons">
|
||||||
{!!privateNote && (
|
{!!privateNote && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="private-note-tag"
|
class="private-note-tag"
|
||||||
title="Private note"
|
title={t`Private note`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setShowPrivateNoteModal(true);
|
setShowPrivateNoteModal(true);
|
||||||
}}
|
}}
|
||||||
|
@ -1056,13 +1112,8 @@ function RelatedActions({
|
||||||
position="anchor"
|
position="anchor"
|
||||||
overflow="auto"
|
overflow="auto"
|
||||||
menuButton={
|
menuButton={
|
||||||
<button
|
<button type="button" class="plain" disabled={loading}>
|
||||||
type="button"
|
<Icon icon="more" size="l" alt={t`More`} />
|
||||||
title="More"
|
|
||||||
class="plain"
|
|
||||||
disabled={loading}
|
|
||||||
>
|
|
||||||
<Icon icon="more" size="l" alt="More" />
|
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
onMenuChange={(e) => {
|
onMenuChange={(e) => {
|
||||||
|
@ -1094,7 +1145,9 @@ function RelatedActions({
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon icon="at" />
|
<Icon icon="at" />
|
||||||
<span>Mention @{username}</span>
|
<span>
|
||||||
|
<Trans>Mention @{username}</Trans>
|
||||||
|
</span>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
@ -1102,7 +1155,9 @@ function RelatedActions({
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon icon="translate" />
|
<Icon icon="translate" />
|
||||||
<span>Translate bio</span>
|
<span>
|
||||||
|
<Trans>Translate bio</Trans>
|
||||||
|
</span>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
{supports('@mastodon/profile-private-note') && (
|
{supports('@mastodon/profile-private-note') && (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
|
@ -1112,7 +1167,7 @@ function RelatedActions({
|
||||||
>
|
>
|
||||||
<Icon icon="pencil" />
|
<Icon icon="pencil" />
|
||||||
<span>
|
<span>
|
||||||
{privateNote ? 'Edit private note' : 'Add private note'}
|
{privateNote ? t`Edit private note` : t`Add private note`}
|
||||||
</span>
|
</span>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
)}
|
)}
|
||||||
|
@ -1132,8 +1187,8 @@ function RelatedActions({
|
||||||
setRelationshipUIState('default');
|
setRelationshipUIState('default');
|
||||||
showToast(
|
showToast(
|
||||||
rel.notifying
|
rel.notifying
|
||||||
? `Notifications enabled for @${username}'s posts.`
|
? t`Notifications enabled for @${username}'s posts.`
|
||||||
: ` Notifications disabled for @${username}'s posts.`,
|
: t` Notifications disabled for @${username}'s posts.`,
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
alert(e);
|
alert(e);
|
||||||
|
@ -1145,8 +1200,8 @@ function RelatedActions({
|
||||||
<Icon icon="notification" />
|
<Icon icon="notification" />
|
||||||
<span>
|
<span>
|
||||||
{notifying
|
{notifying
|
||||||
? 'Disable notifications'
|
? t`Disable notifications`
|
||||||
: 'Enable notifications'}
|
: t`Enable notifications`}
|
||||||
</span>
|
</span>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
|
@ -1163,8 +1218,8 @@ function RelatedActions({
|
||||||
setRelationshipUIState('default');
|
setRelationshipUIState('default');
|
||||||
showToast(
|
showToast(
|
||||||
rel.showingReblogs
|
rel.showingReblogs
|
||||||
? `Boosts from @${username} enabled.`
|
? t`Boosts from @${username} enabled.`
|
||||||
: `Boosts from @${username} disabled.`,
|
: t`Boosts from @${username} disabled.`,
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
alert(e);
|
alert(e);
|
||||||
|
@ -1175,7 +1230,7 @@ function RelatedActions({
|
||||||
>
|
>
|
||||||
<Icon icon="rocket" />
|
<Icon icon="rocket" />
|
||||||
<span>
|
<span>
|
||||||
{showingReblogs ? 'Disable boosts' : 'Enable boosts'}
|
{showingReblogs ? t`Disable boosts` : t`Enable boosts`}
|
||||||
</span>
|
</span>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</>
|
</>
|
||||||
|
@ -1191,7 +1246,7 @@ function RelatedActions({
|
||||||
{lists.length ? (
|
{lists.length ? (
|
||||||
<>
|
<>
|
||||||
<small class="menu-grow">
|
<small class="menu-grow">
|
||||||
Add/Remove from Lists
|
<Trans>Add/Remove from Lists</Trans>
|
||||||
<br />
|
<br />
|
||||||
<span class="more-insignificant">
|
<span class="more-insignificant">
|
||||||
{lists.map((list) => list.title).join(', ')}
|
{lists.map((list) => list.title).join(', ')}
|
||||||
|
@ -1200,7 +1255,9 @@ function RelatedActions({
|
||||||
<small class="more-insignificant">{lists.length}</small>
|
<small class="more-insignificant">{lists.length}</small>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<span>Add/Remove from Lists</span>
|
<span>
|
||||||
|
<Trans>Add/Remove from Lists</Trans>
|
||||||
|
</span>
|
||||||
)}
|
)}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
)}
|
)}
|
||||||
|
@ -1212,16 +1269,16 @@ function RelatedActions({
|
||||||
const handle = `@${currentInfo?.acct || acctWithInstance}`;
|
const handle = `@${currentInfo?.acct || acctWithInstance}`;
|
||||||
try {
|
try {
|
||||||
navigator.clipboard.writeText(handle);
|
navigator.clipboard.writeText(handle);
|
||||||
showToast('Handle copied');
|
showToast(t`Handle copied`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
showToast('Unable to copy handle');
|
showToast(t`Unable to copy handle`);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon icon="copy" />
|
<Icon icon="copy" />
|
||||||
<small>
|
<small>
|
||||||
Copy handle
|
<Trans>Copy handle</Trans>
|
||||||
<br />
|
<br />
|
||||||
<span class="more-insignificant bidi-isolate">
|
<span class="more-insignificant bidi-isolate">
|
||||||
@{currentInfo?.acct || acctWithInstance}
|
@{currentInfo?.acct || acctWithInstance}
|
||||||
|
@ -1238,15 +1295,17 @@ function RelatedActions({
|
||||||
// Copy url to clipboard
|
// Copy url to clipboard
|
||||||
try {
|
try {
|
||||||
navigator.clipboard.writeText(url);
|
navigator.clipboard.writeText(url);
|
||||||
showToast('Link copied');
|
showToast(t`Link copied`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
showToast('Unable to copy link');
|
showToast(t`Unable to copy link`);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon icon="link" />
|
<Icon icon="link" />
|
||||||
<span>Copy</span>
|
<span>
|
||||||
|
<Trans>Copy</Trans>
|
||||||
|
</span>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
{navigator?.share &&
|
{navigator?.share &&
|
||||||
navigator?.canShare?.({
|
navigator?.canShare?.({
|
||||||
|
@ -1260,12 +1319,14 @@ function RelatedActions({
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
alert("Sharing doesn't seem to work.");
|
alert(t`Sharing doesn't seem to work.`);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon icon="share" />
|
<Icon icon="share" />
|
||||||
<span>Share…</span>
|
<span>
|
||||||
|
<Trans>Share…</Trans>
|
||||||
|
</span>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -1284,7 +1345,7 @@ function RelatedActions({
|
||||||
console.log('unmuting', newRelationship);
|
console.log('unmuting', newRelationship);
|
||||||
setRelationship(newRelationship);
|
setRelationship(newRelationship);
|
||||||
setRelationshipUIState('default');
|
setRelationshipUIState('default');
|
||||||
showToast(`Unmuted @${username}`);
|
showToast(t`Unmuted @${username}`);
|
||||||
states.reloadGenericAccounts.id = 'mute';
|
states.reloadGenericAccounts.id = 'mute';
|
||||||
states.reloadGenericAccounts.counter++;
|
states.reloadGenericAccounts.counter++;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -1295,7 +1356,9 @@ function RelatedActions({
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon icon="unmute" />
|
<Icon icon="unmute" />
|
||||||
<span>Unmute @{username}</span>
|
<span>
|
||||||
|
<Trans>Unmute @{username}</Trans>
|
||||||
|
</span>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
) : (
|
) : (
|
||||||
<SubMenu2
|
<SubMenu2
|
||||||
|
@ -1307,7 +1370,9 @@ function RelatedActions({
|
||||||
label={
|
label={
|
||||||
<>
|
<>
|
||||||
<Icon icon="mute" />
|
<Icon icon="mute" />
|
||||||
<span class="menu-grow">Mute @{username}…</span>
|
<span class="menu-grow">
|
||||||
|
<Trans>Mute @{username}…</Trans>
|
||||||
|
</span>
|
||||||
<span
|
<span
|
||||||
style={{
|
style={{
|
||||||
textOverflow: 'clip',
|
textOverflow: 'clip',
|
||||||
|
@ -1336,19 +1401,26 @@ function RelatedActions({
|
||||||
setRelationship(newRelationship);
|
setRelationship(newRelationship);
|
||||||
setRelationshipUIState('default');
|
setRelationshipUIState('default');
|
||||||
showToast(
|
showToast(
|
||||||
`Muted @${username} for ${MUTE_DURATIONS_LABELS[duration]}`,
|
t`Muted @${username} for ${
|
||||||
|
typeof MUTE_DURATIONS_LABELS[duration] ===
|
||||||
|
'function'
|
||||||
|
? MUTE_DURATIONS_LABELS[duration]()
|
||||||
|
: _(MUTE_DURATIONS_LABELS[duration])
|
||||||
|
}`,
|
||||||
);
|
);
|
||||||
states.reloadGenericAccounts.id = 'mute';
|
states.reloadGenericAccounts.id = 'mute';
|
||||||
states.reloadGenericAccounts.counter++;
|
states.reloadGenericAccounts.counter++;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
setRelationshipUIState('error');
|
setRelationshipUIState('error');
|
||||||
showToast(`Unable to mute @${username}`);
|
showToast(t`Unable to mute @${username}`);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{MUTE_DURATIONS_LABELS[duration]}
|
{typeof MUTE_DURATIONS_LABELS[duration] === 'function'
|
||||||
|
? MUTE_DURATIONS_LABELS[duration]()
|
||||||
|
: _(MUTE_DURATIONS_LABELS[duration])}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
@ -1361,7 +1433,9 @@ function RelatedActions({
|
||||||
confirmLabel={
|
confirmLabel={
|
||||||
<>
|
<>
|
||||||
<Icon icon="user-x" />
|
<Icon icon="user-x" />
|
||||||
<span>Remove @{username} from followers?</span>
|
<span>
|
||||||
|
<Trans>Remove @{username} from followers?</Trans>
|
||||||
|
</span>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
@ -1377,7 +1451,7 @@ function RelatedActions({
|
||||||
);
|
);
|
||||||
setRelationship(newRelationship);
|
setRelationship(newRelationship);
|
||||||
setRelationshipUIState('default');
|
setRelationshipUIState('default');
|
||||||
showToast(`@${username} removed from followers`);
|
showToast(t`@${username} removed from followers`);
|
||||||
states.reloadGenericAccounts.id = 'followers';
|
states.reloadGenericAccounts.id = 'followers';
|
||||||
states.reloadGenericAccounts.counter++;
|
states.reloadGenericAccounts.counter++;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -1388,7 +1462,9 @@ function RelatedActions({
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon icon="user-x" />
|
<Icon icon="user-x" />
|
||||||
<span>Remove follower…</span>
|
<span>
|
||||||
|
<Trans>Remove follower…</Trans>
|
||||||
|
</span>
|
||||||
</MenuConfirm>
|
</MenuConfirm>
|
||||||
)}
|
)}
|
||||||
<MenuConfirm
|
<MenuConfirm
|
||||||
|
@ -1397,7 +1473,9 @@ function RelatedActions({
|
||||||
confirmLabel={
|
confirmLabel={
|
||||||
<>
|
<>
|
||||||
<Icon icon="block" />
|
<Icon icon="block" />
|
||||||
<span>Block @{username}?</span>
|
<span>
|
||||||
|
<Trans>Block @{username}?</Trans>
|
||||||
|
</span>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
menuItemClassName="danger"
|
menuItemClassName="danger"
|
||||||
|
@ -1415,7 +1493,7 @@ function RelatedActions({
|
||||||
console.log('unblocking', newRelationship);
|
console.log('unblocking', newRelationship);
|
||||||
setRelationship(newRelationship);
|
setRelationship(newRelationship);
|
||||||
setRelationshipUIState('default');
|
setRelationshipUIState('default');
|
||||||
showToast(`Unblocked @${username}`);
|
showToast(t`Unblocked @${username}`);
|
||||||
} else {
|
} else {
|
||||||
const newRelationship = await currentMasto.v1.accounts
|
const newRelationship = await currentMasto.v1.accounts
|
||||||
.$select(currentInfo?.id || id)
|
.$select(currentInfo?.id || id)
|
||||||
|
@ -1423,7 +1501,7 @@ function RelatedActions({
|
||||||
console.log('blocking', newRelationship);
|
console.log('blocking', newRelationship);
|
||||||
setRelationship(newRelationship);
|
setRelationship(newRelationship);
|
||||||
setRelationshipUIState('default');
|
setRelationshipUIState('default');
|
||||||
showToast(`Blocked @${username}`);
|
showToast(t`Blocked @${username}`);
|
||||||
}
|
}
|
||||||
states.reloadGenericAccounts.id = 'block';
|
states.reloadGenericAccounts.id = 'block';
|
||||||
states.reloadGenericAccounts.counter++;
|
states.reloadGenericAccounts.counter++;
|
||||||
|
@ -1431,9 +1509,9 @@ function RelatedActions({
|
||||||
console.error(e);
|
console.error(e);
|
||||||
setRelationshipUIState('error');
|
setRelationshipUIState('error');
|
||||||
if (blocking) {
|
if (blocking) {
|
||||||
showToast(`Unable to unblock @${username}`);
|
showToast(t`Unable to unblock @${username}`);
|
||||||
} else {
|
} else {
|
||||||
showToast(`Unable to block @${username}`);
|
showToast(t`Unable to block @${username}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
@ -1442,12 +1520,16 @@ function RelatedActions({
|
||||||
{blocking ? (
|
{blocking ? (
|
||||||
<>
|
<>
|
||||||
<Icon icon="unblock" />
|
<Icon icon="unblock" />
|
||||||
<span>Unblock @{username}</span>
|
<span>
|
||||||
|
<Trans>Unblock @{username}</Trans>
|
||||||
|
</span>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Icon icon="block" />
|
<Icon icon="block" />
|
||||||
<span>Block @{username}…</span>
|
<span>
|
||||||
|
<Trans>Block @{username}…</Trans>
|
||||||
|
</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</MenuConfirm>
|
</MenuConfirm>
|
||||||
|
@ -1460,7 +1542,9 @@ function RelatedActions({
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon icon="flag" />
|
<Icon icon="flag" />
|
||||||
<span>Report @{username}…</span>
|
<span>
|
||||||
|
<Trans>Report @{username}…</Trans>
|
||||||
|
</span>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
@ -1476,7 +1560,9 @@ function RelatedActions({
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon icon="pencil" />
|
<Icon icon="pencil" />
|
||||||
<span>Edit profile</span>
|
<span>
|
||||||
|
<Trans>Edit profile</Trans>
|
||||||
|
</span>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
@ -1511,8 +1597,8 @@ function RelatedActions({
|
||||||
confirmLabel={
|
confirmLabel={
|
||||||
<span>
|
<span>
|
||||||
{requested
|
{requested
|
||||||
? 'Withdraw follow request?'
|
? t`Withdraw follow request?`
|
||||||
: `Unfollow @${info.acct || info.username}?`}
|
: t`Unfollow @${info.acct || info.username}?`}
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
menuItemClassName="danger"
|
menuItemClassName="danger"
|
||||||
|
@ -1559,20 +1645,31 @@ function RelatedActions({
|
||||||
>
|
>
|
||||||
{following ? (
|
{following ? (
|
||||||
<>
|
<>
|
||||||
<span>Following</span>
|
<span>
|
||||||
<span>Unfollow…</span>
|
<Trans>Following</Trans>
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
<Trans>Unfollow…</Trans>
|
||||||
|
</span>
|
||||||
</>
|
</>
|
||||||
) : requested ? (
|
) : requested ? (
|
||||||
<>
|
<>
|
||||||
<span>Requested</span>
|
<span>
|
||||||
<span>Withdraw…</span>
|
<Trans>Requested</Trans>
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
<Trans>Withdraw…</Trans>
|
||||||
|
</span>
|
||||||
</>
|
</>
|
||||||
) : locked ? (
|
) : locked ? (
|
||||||
<>
|
<>
|
||||||
<Icon icon="lock" /> <span>Follow</span>
|
<Icon icon="lock" />{' '}
|
||||||
|
<span>
|
||||||
|
<Trans>Follow</Trans>
|
||||||
|
</span>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
'Follow'
|
t`Follow`
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
</MenuConfirm>
|
</MenuConfirm>
|
||||||
|
@ -1683,11 +1780,13 @@ function TranslatedBioSheet({ note, fields, onClose }) {
|
||||||
<div class="sheet">
|
<div class="sheet">
|
||||||
{!!onClose && (
|
{!!onClose && (
|
||||||
<button type="button" class="sheet-close" onClick={onClose}>
|
<button type="button" class="sheet-close" onClick={onClose}>
|
||||||
<Icon icon="x" />
|
<Icon icon="x" alt={t`Close`} />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<header>
|
<header>
|
||||||
<h2>Translated Bio</h2>
|
<h2>
|
||||||
|
<Trans>Translated Bio</Trans>
|
||||||
|
</h2>
|
||||||
</header>
|
</header>
|
||||||
<main>
|
<main>
|
||||||
<p
|
<p
|
||||||
|
@ -1735,11 +1834,13 @@ function AddRemoveListsSheet({ accountID, onClose }) {
|
||||||
<div class="sheet" id="list-add-remove-container">
|
<div class="sheet" id="list-add-remove-container">
|
||||||
{!!onClose && (
|
{!!onClose && (
|
||||||
<button type="button" class="sheet-close" onClick={onClose}>
|
<button type="button" class="sheet-close" onClick={onClose}>
|
||||||
<Icon icon="x" />
|
<Icon icon="x" alt={t`Close`} />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<header>
|
<header>
|
||||||
<h2>Add/Remove from Lists</h2>
|
<h2>
|
||||||
|
<Trans>Add/Remove from Lists</Trans>
|
||||||
|
</h2>
|
||||||
</header>
|
</header>
|
||||||
<main>
|
<main>
|
||||||
{lists.length > 0 ? (
|
{lists.length > 0 ? (
|
||||||
|
@ -1778,14 +1879,14 @@ function AddRemoveListsSheet({ accountID, onClose }) {
|
||||||
setUIState('error');
|
setUIState('error');
|
||||||
alert(
|
alert(
|
||||||
inList
|
inList
|
||||||
? 'Unable to remove from list.'
|
? t`Unable to remove from list.`
|
||||||
: 'Unable to add to list.',
|
: t`Unable to add to list.`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon icon="check-circle" />
|
<Icon icon="check-circle" alt="☑️" />
|
||||||
<span>{list.title}</span>
|
<span>{list.title}</span>
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
|
@ -1797,9 +1898,13 @@ function AddRemoveListsSheet({ accountID, onClose }) {
|
||||||
<Loader abrupt />
|
<Loader abrupt />
|
||||||
</p>
|
</p>
|
||||||
) : uiState === 'error' ? (
|
) : uiState === 'error' ? (
|
||||||
<p class="ui-state">Unable to load lists.</p>
|
<p class="ui-state">
|
||||||
|
<Trans>Unable to load lists.</Trans>
|
||||||
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<p class="ui-state">No lists.</p>
|
<p class="ui-state">
|
||||||
|
<Trans>No lists.</Trans>
|
||||||
|
</p>
|
||||||
)}
|
)}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
@ -1807,7 +1912,10 @@ function AddRemoveListsSheet({ accountID, onClose }) {
|
||||||
onClick={() => setShowListAddEditModal(true)}
|
onClick={() => setShowListAddEditModal(true)}
|
||||||
disabled={uiState !== 'default'}
|
disabled={uiState !== 'default'}
|
||||||
>
|
>
|
||||||
<Icon icon="plus" size="l" /> <span>New list</span>
|
<Icon icon="plus" size="l" />{' '}
|
||||||
|
<span>
|
||||||
|
<Trans>New list</Trans>
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</main>
|
</main>
|
||||||
{showListAddEditModal && (
|
{showListAddEditModal && (
|
||||||
|
@ -1859,11 +1967,15 @@ function PrivateNoteSheet({
|
||||||
<div class="sheet" id="private-note-container">
|
<div class="sheet" id="private-note-container">
|
||||||
{!!onClose && (
|
{!!onClose && (
|
||||||
<button type="button" class="sheet-close" onClick={onClose}>
|
<button type="button" class="sheet-close" onClick={onClose}>
|
||||||
<Icon icon="x" />
|
<Icon icon="x" alt={t`Close`} />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<header>
|
<header>
|
||||||
<b>Private note about @{account?.username || account?.acct}</b>
|
<b>
|
||||||
|
<Trans>
|
||||||
|
Private note about @{account?.username || account?.acct}
|
||||||
|
</Trans>
|
||||||
|
</b>
|
||||||
</header>
|
</header>
|
||||||
<main>
|
<main>
|
||||||
<form
|
<form
|
||||||
|
@ -1887,7 +1999,7 @@ function PrivateNoteSheet({
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
setUIState('error');
|
setUIState('error');
|
||||||
alert(e?.message || 'Unable to update private note.');
|
alert(e?.message || t`Unable to update private note.`);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
|
@ -1910,12 +2022,12 @@ function PrivateNoteSheet({
|
||||||
onClose?.();
|
onClose?.();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Cancel
|
<Trans>Cancel</Trans>
|
||||||
</button>
|
</button>
|
||||||
<span>
|
<span>
|
||||||
<Loader abrupt hidden={uiState !== 'loading'} />
|
<Loader abrupt hidden={uiState !== 'loading'} />
|
||||||
<button disabled={uiState === 'loading'} type="submit">
|
<button disabled={uiState === 'loading'} type="submit">
|
||||||
Save & close
|
<Trans>Save & close</Trans>
|
||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
</footer>
|
</footer>
|
||||||
|
@ -1952,11 +2064,13 @@ function EditProfileSheet({ onClose = () => {} }) {
|
||||||
<div class="sheet" id="edit-profile-container">
|
<div class="sheet" id="edit-profile-container">
|
||||||
{!!onClose && (
|
{!!onClose && (
|
||||||
<button type="button" class="sheet-close" onClick={onClose}>
|
<button type="button" class="sheet-close" onClick={onClose}>
|
||||||
<Icon icon="x" />
|
<Icon icon="x" alt={t`Close`} />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<header>
|
<header>
|
||||||
<b>Edit profile</b>
|
<b>
|
||||||
|
<Trans>Edit profile</Trans>
|
||||||
|
</b>
|
||||||
</header>
|
</header>
|
||||||
<main>
|
<main>
|
||||||
{uiState === 'loading' ? (
|
{uiState === 'loading' ? (
|
||||||
|
@ -2006,7 +2120,7 @@ function EditProfileSheet({ onClose = () => {} }) {
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
alert(e?.message || 'Unable to update profile.');
|
alert(e?.message || t`Unable to update profile.`);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
}}
|
}}
|
||||||
|
@ -2026,7 +2140,7 @@ function EditProfileSheet({ onClose = () => {} }) {
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<label>
|
<label>
|
||||||
Bio
|
<Trans>Bio</Trans>
|
||||||
<textarea
|
<textarea
|
||||||
defaultValue={note}
|
defaultValue={note}
|
||||||
name="note"
|
name="note"
|
||||||
|
@ -2038,12 +2152,18 @@ function EditProfileSheet({ onClose = () => {} }) {
|
||||||
</label>
|
</label>
|
||||||
</p>
|
</p>
|
||||||
{/* Table for fields; name and values are in fields, min 4 rows */}
|
{/* Table for fields; name and values are in fields, min 4 rows */}
|
||||||
<p>Extra fields</p>
|
<p>
|
||||||
|
<Trans>Extra fields</Trans>
|
||||||
|
</p>
|
||||||
<table ref={fieldsAttributesRef}>
|
<table ref={fieldsAttributesRef}>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Label</th>
|
<th>
|
||||||
<th>Content</th>
|
<Trans>Label</Trans>
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
<Trans>Content</Trans>
|
||||||
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
@ -2072,10 +2192,10 @@ function EditProfileSheet({ onClose = () => {} }) {
|
||||||
onClose?.();
|
onClose?.();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Cancel
|
<Trans>Cancel</Trans>
|
||||||
</button>
|
</button>
|
||||||
<button type="submit" disabled={uiState === 'loading'}>
|
<button type="submit" disabled={uiState === 'loading'}>
|
||||||
Save
|
<Trans>Save</Trans>
|
||||||
</button>
|
</button>
|
||||||
</footer>
|
</footer>
|
||||||
</form>
|
</form>
|
||||||
|
@ -2128,10 +2248,11 @@ function AccountHandleInfo({ acct, instance }) {
|
||||||
</span>
|
</span>
|
||||||
<div class="handle-legend">
|
<div class="handle-legend">
|
||||||
<span class="ib">
|
<span class="ib">
|
||||||
<span class="handle-legend-icon username" /> username
|
<span class="handle-legend-icon username" /> <Trans>username</Trans>
|
||||||
</span>{' '}
|
</span>{' '}
|
||||||
<span class="ib">
|
<span class="ib">
|
||||||
<span class="handle-legend-icon server" /> server domain name
|
<span class="handle-legend-icon server" />{' '}
|
||||||
|
<Trans>server domain name</Trans>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { t } from '@lingui/macro';
|
||||||
import { useEffect } from 'preact/hooks';
|
import { useEffect } from 'preact/hooks';
|
||||||
|
|
||||||
import { api } from '../utils/api';
|
import { api } from '../utils/api';
|
||||||
|
@ -33,7 +34,7 @@ function AccountSheet({ account, instance: propInstance, onClose }) {
|
||||||
>
|
>
|
||||||
{!!onClose && (
|
{!!onClose && (
|
||||||
<button type="button" class="sheet-close outer" onClick={onClose}>
|
<button type="button" class="sheet-close outer" onClick={onClose}>
|
||||||
<Icon icon="x" />
|
<Icon icon="x" alt={t`Close`} />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<AccountInfo
|
<AccountInfo
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { t, Trans } from '@lingui/macro';
|
||||||
import { memo } from 'preact/compat';
|
import { memo } from 'preact/compat';
|
||||||
import { useEffect, useRef, useState } from 'preact/hooks';
|
import { useEffect, useRef, useState } from 'preact/hooks';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
|
@ -134,7 +135,7 @@ export default memo(function BackgroundService({ isLoggedIn }) {
|
||||||
const currentCloakMode = states.settings.cloakMode;
|
const currentCloakMode = states.settings.cloakMode;
|
||||||
states.settings.cloakMode = !currentCloakMode;
|
states.settings.cloakMode = !currentCloakMode;
|
||||||
showToast({
|
showToast({
|
||||||
text: `Cloak mode ${currentCloakMode ? 'disabled' : 'enabled'}`,
|
text: currentCloakMode ? t`Cloak mode disabled` : t`Cloak mode enabled`,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { t, Trans } from '@lingui/macro';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
import { useSnapshot } from 'valtio';
|
import { useSnapshot } from 'valtio';
|
||||||
|
|
||||||
|
@ -15,7 +16,7 @@ import states from '../utils/states';
|
||||||
import useTitle from '../utils/useTitle';
|
import useTitle from '../utils/useTitle';
|
||||||
|
|
||||||
function Columns() {
|
function Columns() {
|
||||||
useTitle('Home', '/');
|
useTitle(t`Home`, '/');
|
||||||
const snapStates = useSnapshot(states);
|
const snapStates = useSnapshot(states);
|
||||||
const { shortcuts } = snapStates;
|
const { shortcuts } = snapStates;
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { t, Trans } from '@lingui/macro';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
import { useSnapshot } from 'valtio';
|
import { useSnapshot } from 'valtio';
|
||||||
|
|
||||||
|
@ -45,7 +46,7 @@ export default function ComposeButton() {
|
||||||
snapStates.composerState.publishing ? 'loading' : ''
|
snapStates.composerState.publishing ? 'loading' : ''
|
||||||
} ${snapStates.composerState.publishingError ? 'error' : ''}`}
|
} ${snapStates.composerState.publishingError ? 'error' : ''}`}
|
||||||
>
|
>
|
||||||
<Icon icon="quill" size="xl" alt="Compose" />
|
<Icon icon="quill" size="xl" alt={t`Compose`} />
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import './compose.css';
|
import './compose.css';
|
||||||
import '@github/text-expander-element';
|
import '@github/text-expander-element';
|
||||||
|
|
||||||
|
import { msg, plural, t, Trans } from '@lingui/macro';
|
||||||
|
import { useLingui } from '@lingui/react';
|
||||||
import { MenuItem } from '@szhsin/react-menu';
|
import { MenuItem } from '@szhsin/react-menu';
|
||||||
import { deepEqual } from 'fast-equals';
|
import { deepEqual } from 'fast-equals';
|
||||||
import Fuse from 'fuse.js';
|
import Fuse from 'fuse.js';
|
||||||
|
@ -27,11 +29,14 @@ import urlRegex from '../data/url-regex';
|
||||||
import { api } from '../utils/api';
|
import { api } from '../utils/api';
|
||||||
import db from '../utils/db';
|
import db from '../utils/db';
|
||||||
import emojifyText from '../utils/emojify-text';
|
import emojifyText from '../utils/emojify-text';
|
||||||
|
import i18nDuration from '../utils/i18n-duration';
|
||||||
import isRTL from '../utils/is-rtl';
|
import isRTL from '../utils/is-rtl';
|
||||||
import localeMatch from '../utils/locale-match';
|
import localeMatch from '../utils/locale-match';
|
||||||
import localeCode2Text from '../utils/localeCode2Text';
|
import localeCode2Text from '../utils/localeCode2Text';
|
||||||
|
import mem from '../utils/mem';
|
||||||
import openCompose from '../utils/open-compose';
|
import openCompose from '../utils/open-compose';
|
||||||
import pmem from '../utils/pmem';
|
import pmem from '../utils/pmem';
|
||||||
|
import prettyBytes from '../utils/pretty-bytes';
|
||||||
import { fetchRelationships } from '../utils/relationships';
|
import { fetchRelationships } from '../utils/relationships';
|
||||||
import shortenNumber from '../utils/shorten-number';
|
import shortenNumber from '../utils/shorten-number';
|
||||||
import showToast from '../utils/show-toast';
|
import showToast from '../utils/show-toast';
|
||||||
|
@ -74,16 +79,15 @@ const supportedLanguagesMap = supportedLanguages.reduce((acc, l) => {
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const expiryOptions = {
|
const expiryOptions = {
|
||||||
'5 minutes': 5 * 60,
|
300: i18nDuration(5, 'minute'),
|
||||||
'30 minutes': 30 * 60,
|
1_800: i18nDuration(30, 'minute'),
|
||||||
'1 hour': 60 * 60,
|
3_600: i18nDuration(1, 'hour'),
|
||||||
'6 hours': 6 * 60 * 60,
|
21_600: i18nDuration(6, 'hour'),
|
||||||
'12 hours': 12 * 60 * 60,
|
86_400: i18nDuration(1, 'day'),
|
||||||
'1 day': 24 * 60 * 60,
|
259_200: i18nDuration(3, 'day'),
|
||||||
'3 days': 3 * 24 * 60 * 60,
|
604_800: i18nDuration(1, 'week'),
|
||||||
'7 days': 7 * 24 * 60 * 60,
|
|
||||||
};
|
};
|
||||||
const expirySeconds = Object.values(expiryOptions);
|
const expirySeconds = Object.keys(expiryOptions);
|
||||||
const oneDay = 24 * 60 * 60;
|
const oneDay = 24 * 60 * 60;
|
||||||
|
|
||||||
const expiresInFromExpiresAt = (expiresAt) => {
|
const expiresInFromExpiresAt = (expiresAt) => {
|
||||||
|
@ -191,7 +195,8 @@ function highlightText(text, { maxCharacters = Infinity }) {
|
||||||
); // Emoji shortcodes
|
); // Emoji shortcodes
|
||||||
}
|
}
|
||||||
|
|
||||||
const rtf = new Intl.RelativeTimeFormat();
|
// const rtf = new Intl.RelativeTimeFormat();
|
||||||
|
const RTF = mem((locale) => new Intl.RelativeTimeFormat(locale || undefined));
|
||||||
|
|
||||||
const CUSTOM_EMOJIS_COUNT = 100;
|
const CUSTOM_EMOJIS_COUNT = 100;
|
||||||
|
|
||||||
|
@ -203,6 +208,9 @@ function Compose({
|
||||||
standalone,
|
standalone,
|
||||||
hasOpener,
|
hasOpener,
|
||||||
}) {
|
}) {
|
||||||
|
const { i18n } = useLingui();
|
||||||
|
const rtf = RTF(i18n.locale);
|
||||||
|
|
||||||
console.warn('RENDER COMPOSER');
|
console.warn('RENDER COMPOSER');
|
||||||
const { masto, instance } = api();
|
const { masto, instance } = api();
|
||||||
const [uiState, setUIState] = useState('default');
|
const [uiState, setUIState] = useState('default');
|
||||||
|
@ -381,7 +389,7 @@ function Compose({
|
||||||
|
|
||||||
const formRef = useRef();
|
const formRef = useRef();
|
||||||
|
|
||||||
const beforeUnloadCopy = 'You have unsaved changes. Discard this post?';
|
const beforeUnloadCopy = t`You have unsaved changes. Discard this post?`;
|
||||||
const canClose = () => {
|
const canClose = () => {
|
||||||
const { value, dataset } = textareaRef.current;
|
const { value, dataset } = textareaRef.current;
|
||||||
|
|
||||||
|
@ -602,7 +610,12 @@ function Compose({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (files.length > 0 && mediaAttachments.length >= maxMediaAttachments) {
|
if (files.length > 0 && mediaAttachments.length >= maxMediaAttachments) {
|
||||||
alert(`You can only attach up to ${maxMediaAttachments} files.`);
|
alert(
|
||||||
|
plural(maxMediaAttachments, {
|
||||||
|
one: 'You can only attach up to 1 file.',
|
||||||
|
other: 'You can only attach up to # files.',
|
||||||
|
}),
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.log({ files });
|
console.log({ files });
|
||||||
|
@ -613,7 +626,12 @@ function Compose({
|
||||||
const max = maxMediaAttachments - mediaAttachments.length;
|
const max = maxMediaAttachments - mediaAttachments.length;
|
||||||
const allowedFiles = files.slice(0, max);
|
const allowedFiles = files.slice(0, max);
|
||||||
if (allowedFiles.length <= 0) {
|
if (allowedFiles.length <= 0) {
|
||||||
alert(`You can only attach up to ${maxMediaAttachments} files.`);
|
alert(
|
||||||
|
plural(maxMediaAttachments, {
|
||||||
|
one: 'You can only attach up to 1 file.',
|
||||||
|
other: 'You can only attach up to # files.',
|
||||||
|
}),
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const mediaFiles = allowedFiles.map((file) => ({
|
const mediaFiles = allowedFiles.map((file) => ({
|
||||||
|
@ -757,14 +775,14 @@ function Compose({
|
||||||
onClose();
|
onClose();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon icon="popout" alt="Pop out" />
|
<Icon icon="popout" alt={t`Pop out`} />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="plain4 min-button"
|
class="plain4 min-button"
|
||||||
onClick={onMinimize}
|
onClick={onMinimize}
|
||||||
>
|
>
|
||||||
<Icon icon="minimize" alt="Minimize" />
|
<Icon icon="minimize" alt={t`Minimize`} />
|
||||||
</button>{' '}
|
</button>{' '}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
@ -776,7 +794,7 @@ function Compose({
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon icon="x" />
|
<Icon icon="x" alt={t`Close`} />
|
||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
|
@ -800,20 +818,19 @@ function Compose({
|
||||||
// }
|
// }
|
||||||
|
|
||||||
if (!window.opener) {
|
if (!window.opener) {
|
||||||
alert('Looks like you closed the parent window.');
|
alert(t`Looks like you closed the parent window.`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (window.opener.__STATES__.showCompose) {
|
if (window.opener.__STATES__.showCompose) {
|
||||||
if (window.opener.__STATES__.composerState?.publishing) {
|
if (window.opener.__STATES__.composerState?.publishing) {
|
||||||
alert(
|
alert(
|
||||||
'Looks like you already have a compose field open in the parent window and currently publishing. Please wait for it to be done and try again later.',
|
t`Looks like you already have a compose field open in the parent window and currently publishing. Please wait for it to be done and try again later.`,
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let confirmText =
|
let confirmText = t`Looks like you already have a compose field open in the parent window. Popping in this window will discard the changes you made in the parent window. Continue?`;
|
||||||
'Looks like you already have a compose field open in the parent window. Popping in this window will discard the changes you made in the parent window. Continue?';
|
|
||||||
const yes = confirm(confirmText);
|
const yes = confirm(confirmText);
|
||||||
if (!yes) return;
|
if (!yes) return;
|
||||||
}
|
}
|
||||||
|
@ -855,7 +872,7 @@ function Compose({
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon icon="popin" alt="Pop in" />
|
<Icon icon="popin" alt={t`Pop in`} />
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
|
@ -864,18 +881,22 @@ function Compose({
|
||||||
<div class="status-preview">
|
<div class="status-preview">
|
||||||
<Status status={replyToStatus} size="s" previewMode />
|
<Status status={replyToStatus} size="s" previewMode />
|
||||||
<div class="status-preview-legend reply-to">
|
<div class="status-preview-legend reply-to">
|
||||||
|
{replyToStatusMonthsAgo > 0 ? (
|
||||||
|
<Trans>
|
||||||
Replying to @
|
Replying to @
|
||||||
{replyToStatus.account.acct || replyToStatus.account.username}
|
{replyToStatus.account.acct || replyToStatus.account.username}
|
||||||
’s post
|
’s post (
|
||||||
{replyToStatusMonthsAgo >= 3 && (
|
|
||||||
<>
|
|
||||||
{' '}
|
|
||||||
(
|
|
||||||
<strong>
|
<strong>
|
||||||
{rtf.format(-replyToStatusMonthsAgo, 'month')}
|
{rtf.format(-replyToStatusMonthsAgo, 'month')}
|
||||||
</strong>
|
</strong>
|
||||||
)
|
)
|
||||||
</>
|
</Trans>
|
||||||
|
) : (
|
||||||
|
<Trans>
|
||||||
|
Replying to @
|
||||||
|
{replyToStatus.account.acct || replyToStatus.account.username}
|
||||||
|
’s post
|
||||||
|
</Trans>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -883,7 +904,9 @@ function Compose({
|
||||||
{!!editStatus && (
|
{!!editStatus && (
|
||||||
<div class="status-preview">
|
<div class="status-preview">
|
||||||
<Status status={editStatus} size="s" previewMode />
|
<Status status={editStatus} size="s" previewMode />
|
||||||
<div class="status-preview-legend">Editing source post</div>
|
<div class="status-preview-legend">
|
||||||
|
<Trans>Editing source post</Trans>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<form
|
<form
|
||||||
|
@ -929,11 +952,11 @@ function Compose({
|
||||||
*/
|
*/
|
||||||
if (poll) {
|
if (poll) {
|
||||||
if (poll.options.length < 2) {
|
if (poll.options.length < 2) {
|
||||||
alert('Poll must have at least 2 options');
|
alert(t`Poll must have at least 2 options`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (poll.options.some((option) => option === '')) {
|
if (poll.options.some((option) => option === '')) {
|
||||||
alert('Some poll choices are empty');
|
alert(t`Some poll choices are empty`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -946,7 +969,7 @@ function Compose({
|
||||||
);
|
);
|
||||||
if (hasNoDescriptions) {
|
if (hasNoDescriptions) {
|
||||||
const yes = confirm(
|
const yes = confirm(
|
||||||
'Some media have no descriptions. Continue?',
|
t`Some media have no descriptions. Continue?`,
|
||||||
);
|
);
|
||||||
if (!yes) return;
|
if (!yes) return;
|
||||||
}
|
}
|
||||||
|
@ -998,7 +1021,7 @@ function Compose({
|
||||||
results.forEach((result) => {
|
results.forEach((result) => {
|
||||||
if (result.status === 'rejected') {
|
if (result.status === 'rejected') {
|
||||||
console.error(result);
|
console.error(result);
|
||||||
alert(result.reason || `Attachment #${i} failed`);
|
alert(result.reason || t`Attachment #${i} failed`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
|
@ -1092,7 +1115,7 @@ function Compose({
|
||||||
ref={spoilerTextRef}
|
ref={spoilerTextRef}
|
||||||
type="text"
|
type="text"
|
||||||
name="spoilerText"
|
name="spoilerText"
|
||||||
placeholder="Content warning"
|
placeholder={t`Content warning`}
|
||||||
disabled={uiState === 'loading'}
|
disabled={uiState === 'loading'}
|
||||||
class="spoiler-text-field"
|
class="spoiler-text-field"
|
||||||
lang={language}
|
lang={language}
|
||||||
|
@ -1108,7 +1131,7 @@ function Compose({
|
||||||
/>
|
/>
|
||||||
<label
|
<label
|
||||||
class={`toolbar-button ${sensitive ? 'highlight' : ''}`}
|
class={`toolbar-button ${sensitive ? 'highlight' : ''}`}
|
||||||
title="Content warning or sensitive media"
|
title={t`Content warning or sensitive media`}
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
name="sensitive"
|
name="sensitive"
|
||||||
|
@ -1144,11 +1167,17 @@ function Compose({
|
||||||
dir="auto"
|
dir="auto"
|
||||||
>
|
>
|
||||||
<option value="public">
|
<option value="public">
|
||||||
Public <Icon icon="earth" />
|
<Trans>Public</Trans>
|
||||||
|
</option>
|
||||||
|
<option value="unlisted">
|
||||||
|
<Trans>Unlisted</Trans>
|
||||||
|
</option>
|
||||||
|
<option value="private">
|
||||||
|
<Trans>Followers only</Trans>
|
||||||
|
</option>
|
||||||
|
<option value="direct">
|
||||||
|
<Trans>Private mention</Trans>
|
||||||
</option>
|
</option>
|
||||||
<option value="unlisted">Unlisted</option>
|
|
||||||
<option value="private">Followers only</option>
|
|
||||||
<option value="direct">Private mention</option>
|
|
||||||
</select>
|
</select>
|
||||||
</label>{' '}
|
</label>{' '}
|
||||||
</div>
|
</div>
|
||||||
|
@ -1156,10 +1185,10 @@ function Compose({
|
||||||
ref={textareaRef}
|
ref={textareaRef}
|
||||||
placeholder={
|
placeholder={
|
||||||
replyToStatus
|
replyToStatus
|
||||||
? 'Post your reply'
|
? t`Post your reply`
|
||||||
: editStatus
|
: editStatus
|
||||||
? 'Edit your post'
|
? t`Edit your post`
|
||||||
: 'What are you doing?'
|
: t`What are you doing?`
|
||||||
}
|
}
|
||||||
required={mediaAttachments?.length === 0}
|
required={mediaAttachments?.length === 0}
|
||||||
disabled={uiState === 'loading'}
|
disabled={uiState === 'loading'}
|
||||||
|
@ -1233,7 +1262,9 @@ function Compose({
|
||||||
setSensitive(sensitive);
|
setSensitive(sensitive);
|
||||||
}}
|
}}
|
||||||
/>{' '}
|
/>{' '}
|
||||||
<span>Mark media as sensitive</span>{' '}
|
<span>
|
||||||
|
<Trans>Mark media as sensitive</Trans>
|
||||||
|
</span>{' '}
|
||||||
<Icon icon={`eye-${sensitive ? 'close' : 'open'}`} />
|
<Icon icon={`eye-${sensitive ? 'close' : 'open'}`} />
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1294,7 +1325,10 @@ function Compose({
|
||||||
maxMediaAttachments
|
maxMediaAttachments
|
||||||
) {
|
) {
|
||||||
alert(
|
alert(
|
||||||
`You can only attach up to ${maxMediaAttachments} files.`,
|
plural(maxMediaAttachments, {
|
||||||
|
one: 'You can only attach up to 1 file.',
|
||||||
|
other: 'You can only attach up to # files.',
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
setMediaAttachments((attachments) => {
|
setMediaAttachments((attachments) => {
|
||||||
|
@ -1327,7 +1361,7 @@ function Compose({
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon icon="poll" alt="Add poll" />
|
<Icon icon="poll" alt={t`Add poll`} />
|
||||||
</button>
|
</button>
|
||||||
</>
|
</>
|
||||||
))}
|
))}
|
||||||
|
@ -1349,7 +1383,7 @@ function Compose({
|
||||||
setShowEmoji2Picker(true);
|
setShowEmoji2Picker(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon icon="emoji2" />
|
<Icon icon="emoji2" alt={t`Add custom emoji`} />
|
||||||
</button>
|
</button>
|
||||||
{!!states.settings.composerGIFPicker && (
|
{!!states.settings.composerGIFPicker && (
|
||||||
<button
|
<button
|
||||||
|
@ -1400,17 +1434,31 @@ function Compose({
|
||||||
disabled={uiState === 'loading'}
|
disabled={uiState === 'loading'}
|
||||||
dir="auto"
|
dir="auto"
|
||||||
>
|
>
|
||||||
{topSupportedLanguages.map(([code, common, native]) => (
|
{topSupportedLanguages.map(([code, common, native]) => {
|
||||||
|
const commonText = localeCode2Text({
|
||||||
|
code,
|
||||||
|
fallback: common,
|
||||||
|
});
|
||||||
|
const same = commonText === native;
|
||||||
|
return (
|
||||||
<option value={code} key={code}>
|
<option value={code} key={code}>
|
||||||
{common} ({native})
|
{same ? commonText : `${commonText} (${native})`}
|
||||||
</option>
|
</option>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
<hr />
|
<hr />
|
||||||
{restSupportedLanguages.map(([code, common, native]) => (
|
{restSupportedLanguages.map(([code, common, native]) => {
|
||||||
|
const commonText = localeCode2Text({
|
||||||
|
code,
|
||||||
|
fallback: common,
|
||||||
|
});
|
||||||
|
const same = commonText === native;
|
||||||
|
return (
|
||||||
<option value={code} key={code}>
|
<option value={code} key={code}>
|
||||||
{common} ({native})
|
{same ? commonText : `${commonText} (${native})`}
|
||||||
</option>
|
</option>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</select>
|
</select>
|
||||||
</label>{' '}
|
</label>{' '}
|
||||||
<button
|
<button
|
||||||
|
@ -1418,7 +1466,7 @@ function Compose({
|
||||||
class="large"
|
class="large"
|
||||||
disabled={uiState === 'loading'}
|
disabled={uiState === 'loading'}
|
||||||
>
|
>
|
||||||
{replyToStatus ? 'Reply' : editStatus ? 'Update' : 'Post'}
|
{replyToStatus ? t`Reply` : editStatus ? t`Update` : t`Post`}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
@ -1531,7 +1579,10 @@ function Compose({
|
||||||
console.log('GIF URL', url);
|
console.log('GIF URL', url);
|
||||||
if (mediaAttachments.length >= maxMediaAttachments) {
|
if (mediaAttachments.length >= maxMediaAttachments) {
|
||||||
alert(
|
alert(
|
||||||
`You can only attach up to ${maxMediaAttachments} files.`,
|
plural(maxMediaAttachments, {
|
||||||
|
one: 'You can only attach up to 1 file.',
|
||||||
|
other: 'You can only attach up to # files.',
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -1540,7 +1591,7 @@ function Compose({
|
||||||
let theToast;
|
let theToast;
|
||||||
try {
|
try {
|
||||||
theToast = showToast({
|
theToast = showToast({
|
||||||
text: 'Downloading GIF…',
|
text: t`Downloading GIF…`,
|
||||||
duration: -1,
|
duration: -1,
|
||||||
});
|
});
|
||||||
const blob = await fetch(url, {
|
const blob = await fetch(url, {
|
||||||
|
@ -1568,7 +1619,7 @@ function Compose({
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
theToast?.hideToast?.();
|
theToast?.hideToast?.();
|
||||||
showToast('Failed to download GIF');
|
showToast(t`Failed to download GIF`);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
}}
|
}}
|
||||||
|
@ -1679,7 +1730,7 @@ const Textarea = forwardRef((props, ref) => {
|
||||||
${encodeHTML(shortcode)}
|
${encodeHTML(shortcode)}
|
||||||
</li>`;
|
</li>`;
|
||||||
});
|
});
|
||||||
html += `<li role="option" data-value="" data-more="${text}">More…</li>`;
|
html += `<li role="option" data-value="" data-more="${text}">${t`More…`}</li>`;
|
||||||
// console.log({ emojis, html });
|
// console.log({ emojis, html });
|
||||||
menu.innerHTML = html;
|
menu.innerHTML = html;
|
||||||
provide(
|
provide(
|
||||||
|
@ -1756,7 +1807,7 @@ const Textarea = forwardRef((props, ref) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (type === 'accounts') {
|
if (type === 'accounts') {
|
||||||
html += `<li role="option" data-value="" data-more="${text}">More…</li>`;
|
html += `<li role="option" data-value="" data-more="${text}">${t`More…`}</li>`;
|
||||||
}
|
}
|
||||||
menu.innerHTML = html;
|
menu.innerHTML = html;
|
||||||
console.log('MENU', results, menu);
|
console.log('MENU', results, menu);
|
||||||
|
@ -2029,16 +2080,6 @@ function CharCountMeter({ maxCharacters = 500, hidden }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function prettyBytes(bytes) {
|
|
||||||
const units = ['bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
|
||||||
let unitIndex = 0;
|
|
||||||
while (bytes >= 1024) {
|
|
||||||
bytes /= 1024;
|
|
||||||
unitIndex++;
|
|
||||||
}
|
|
||||||
return `${bytes.toFixed(0).toLocaleString()} ${units[unitIndex]}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function scaleDimension(matrix, matrixLimit, width, height) {
|
function scaleDimension(matrix, matrixLimit, width, height) {
|
||||||
// matrix = number of pixels
|
// matrix = number of pixels
|
||||||
// matrixLimit = max number of pixels
|
// matrixLimit = max number of pixels
|
||||||
|
@ -2056,6 +2097,7 @@ function MediaAttachment({
|
||||||
onDescriptionChange = () => {},
|
onDescriptionChange = () => {},
|
||||||
onRemove = () => {},
|
onRemove = () => {},
|
||||||
}) {
|
}) {
|
||||||
|
const { i18n } = useLingui();
|
||||||
const [uiState, setUIState] = useState('default');
|
const [uiState, setUIState] = useState('default');
|
||||||
const supportsEdit = supports('@mastodon/edit-media-attributes');
|
const supportsEdit = supports('@mastodon/edit-media-attributes');
|
||||||
const { type, id, file } = attachment;
|
const { type, id, file } = attachment;
|
||||||
|
@ -2167,7 +2209,9 @@ function MediaAttachment({
|
||||||
<>
|
<>
|
||||||
{!!id && !supportsEdit ? (
|
{!!id && !supportsEdit ? (
|
||||||
<div class="media-desc">
|
<div class="media-desc">
|
||||||
<span class="tag">Uploaded</span>
|
<span class="tag">
|
||||||
|
<Trans>Uploaded</Trans>
|
||||||
|
</span>
|
||||||
<p title={description}>
|
<p title={description}>
|
||||||
{attachment.description || <i>No description</i>}
|
{attachment.description || <i>No description</i>}
|
||||||
</p>
|
</p>
|
||||||
|
@ -2179,9 +2223,9 @@ function MediaAttachment({
|
||||||
lang={lang}
|
lang={lang}
|
||||||
placeholder={
|
placeholder={
|
||||||
{
|
{
|
||||||
image: 'Image description',
|
image: t`Image description`,
|
||||||
video: 'Video description',
|
video: t`Video description`,
|
||||||
audio: 'Audio description',
|
audio: t`Audio description`,
|
||||||
}[suffixType]
|
}[suffixType]
|
||||||
}
|
}
|
||||||
autoCapitalize="sentences"
|
autoCapitalize="sentences"
|
||||||
|
@ -2217,7 +2261,7 @@ function MediaAttachment({
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'imageSizeLimit': {
|
case 'imageSizeLimit': {
|
||||||
const { imageSize, imageSizeLimit } = details;
|
const { imageSize, imageSizeLimit } = details;
|
||||||
return `File size too large. Uploading might encounter issues. Try reduce the file size from ${prettyBytes(
|
return t`File size too large. Uploading might encounter issues. Try reduce the file size from ${prettyBytes(
|
||||||
imageSize,
|
imageSize,
|
||||||
)} to ${prettyBytes(imageSizeLimit)} or lower.`;
|
)} to ${prettyBytes(imageSizeLimit)} or lower.`;
|
||||||
}
|
}
|
||||||
|
@ -2229,11 +2273,15 @@ function MediaAttachment({
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
);
|
);
|
||||||
return `Dimension too large. Uploading might encounter issues. Try reduce dimension from ${width.toLocaleString()}×${height.toLocaleString()}px to ${newWidth.toLocaleString()}×${newHeight.toLocaleString()}px.`;
|
return t`Dimension too large. Uploading might encounter issues. Try reduce dimension from ${i18n.number(
|
||||||
|
width,
|
||||||
|
)}×${i18n.number(height)}px to ${i18n.number(newWidth)}×${i18n.number(
|
||||||
|
newHeight,
|
||||||
|
)}px.`;
|
||||||
}
|
}
|
||||||
case 'videoSizeLimit': {
|
case 'videoSizeLimit': {
|
||||||
const { videoSize, videoSizeLimit } = details;
|
const { videoSize, videoSizeLimit } = details;
|
||||||
return `File size too large. Uploading might encounter issues. Try reduce the file size from ${prettyBytes(
|
return t`File size too large. Uploading might encounter issues. Try reduce the file size from ${prettyBytes(
|
||||||
videoSize,
|
videoSize,
|
||||||
)} to ${prettyBytes(videoSizeLimit)} or lower.`;
|
)} to ${prettyBytes(videoSizeLimit)} or lower.`;
|
||||||
}
|
}
|
||||||
|
@ -2245,11 +2293,15 @@ function MediaAttachment({
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
);
|
);
|
||||||
return `Dimension too large. Uploading might encounter issues. Try reduce dimension from ${width.toLocaleString()}×${height.toLocaleString()}px to ${newWidth.toLocaleString()}×${newHeight.toLocaleString()}px.`;
|
return t`Dimension too large. Uploading might encounter issues. Try reduce dimension from ${i18n.number(
|
||||||
|
width,
|
||||||
|
)}×${i18n.number(height)}px to ${i18n.number(newWidth)}×${i18n.number(
|
||||||
|
newHeight,
|
||||||
|
)}px.`;
|
||||||
}
|
}
|
||||||
case 'videoFrameRateLimit': {
|
case 'videoFrameRateLimit': {
|
||||||
// Not possible to detect this on client-side for now
|
// Not possible to detect this on client-side for now
|
||||||
return 'Frame rate too high. Uploading might encounter issues.';
|
return t`Frame rate too high. Uploading might encounter issues.`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -2309,7 +2361,7 @@ function MediaAttachment({
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onClick={onRemove}
|
onClick={onRemove}
|
||||||
>
|
>
|
||||||
<Icon icon="x" />
|
<Icon icon="x" alt={t`Remove`} />
|
||||||
</button>
|
</button>
|
||||||
{!!maxError && (
|
{!!maxError && (
|
||||||
<button
|
<button
|
||||||
|
@ -2326,7 +2378,7 @@ function MediaAttachment({
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon icon="alert" />
|
<Icon icon="alert" alt={t`Error`} />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -2345,15 +2397,15 @@ function MediaAttachment({
|
||||||
setShowModal(false);
|
setShowModal(false);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon icon="x" />
|
<Icon icon="x" alt={t`Close`} />
|
||||||
</button>
|
</button>
|
||||||
<header>
|
<header>
|
||||||
<h2>
|
<h2>
|
||||||
{
|
{
|
||||||
{
|
{
|
||||||
image: 'Edit image description',
|
image: t`Edit image description`,
|
||||||
video: 'Edit video description',
|
video: t`Edit video description`,
|
||||||
audio: 'Edit audio description',
|
audio: t`Edit audio description`,
|
||||||
}[suffixType]
|
}[suffixType]
|
||||||
}
|
}
|
||||||
</h2>
|
</h2>
|
||||||
|
@ -2388,8 +2440,8 @@ function MediaAttachment({
|
||||||
position="anchor"
|
position="anchor"
|
||||||
overflow="auto"
|
overflow="auto"
|
||||||
menuButton={
|
menuButton={
|
||||||
<button type="button" title="More" class="plain">
|
<button type="button" class="plain">
|
||||||
<Icon icon="more" size="l" alt="More" />
|
<Icon icon="more" size="l" alt={t`More`} />
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
@ -2398,7 +2450,7 @@ function MediaAttachment({
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setUIState('loading');
|
setUIState('loading');
|
||||||
toastRef.current = showToast({
|
toastRef.current = showToast({
|
||||||
text: 'Generating description. Please wait...',
|
text: t`Generating description. Please wait...`,
|
||||||
duration: -1,
|
duration: -1,
|
||||||
});
|
});
|
||||||
// POST with multipart
|
// POST with multipart
|
||||||
|
@ -2417,9 +2469,9 @@ function MediaAttachment({
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
showToast(
|
showToast(
|
||||||
`Failed to generate description${
|
e.message
|
||||||
e?.message ? `: ${e.message}` : ''
|
? t`Failed to generate description: ${e.message}`
|
||||||
}`,
|
: t`Failed to generate description`,
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
setUIState('default');
|
setUIState('default');
|
||||||
|
@ -2431,12 +2483,14 @@ function MediaAttachment({
|
||||||
<Icon icon="sparkles2" />
|
<Icon icon="sparkles2" />
|
||||||
{lang && lang !== 'en' ? (
|
{lang && lang !== 'en' ? (
|
||||||
<small>
|
<small>
|
||||||
Generate description…
|
<Trans>Generate description…</Trans>
|
||||||
<br />
|
<br />
|
||||||
(English)
|
(English)
|
||||||
</small>
|
</small>
|
||||||
) : (
|
) : (
|
||||||
<span>Generate description…</span>
|
<span>
|
||||||
|
<Trans>Generate description…</Trans>
|
||||||
|
</span>
|
||||||
)}
|
)}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
{!!lang && lang !== 'en' && (
|
{!!lang && lang !== 'en' && (
|
||||||
|
@ -2445,7 +2499,7 @@ function MediaAttachment({
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setUIState('loading');
|
setUIState('loading');
|
||||||
toastRef.current = showToast({
|
toastRef.current = showToast({
|
||||||
text: 'Generating description. Please wait...',
|
text: t`Generating description. Please wait...`,
|
||||||
duration: -1,
|
duration: -1,
|
||||||
});
|
});
|
||||||
// POST with multipart
|
// POST with multipart
|
||||||
|
@ -2468,7 +2522,7 @@ function MediaAttachment({
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
showToast(
|
showToast(
|
||||||
`Failed to generate description${
|
t`Failed to generate description${
|
||||||
e?.message ? `: ${e.message}` : ''
|
e?.message ? `: ${e.message}` : ''
|
||||||
}`,
|
}`,
|
||||||
);
|
);
|
||||||
|
@ -2481,11 +2535,14 @@ function MediaAttachment({
|
||||||
>
|
>
|
||||||
<Icon icon="sparkles2" />
|
<Icon icon="sparkles2" />
|
||||||
<small>
|
<small>
|
||||||
Generate description…
|
<Trans>Generate description…</Trans>
|
||||||
<br />({localeCode2Text(lang)}){' '}
|
<br />
|
||||||
|
<Trans>
|
||||||
|
({localeCode2Text(lang)}){' '}
|
||||||
<span class="more-insignificant">
|
<span class="more-insignificant">
|
||||||
— experimental
|
— experimental
|
||||||
</span>
|
</span>
|
||||||
|
</Trans>
|
||||||
</small>
|
</small>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
)}
|
)}
|
||||||
|
@ -2499,7 +2556,7 @@ function MediaAttachment({
|
||||||
}}
|
}}
|
||||||
disabled={uiState === 'loading'}
|
disabled={uiState === 'loading'}
|
||||||
>
|
>
|
||||||
Done
|
<Trans>Done</Trans>
|
||||||
</button>
|
</button>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
@ -2521,6 +2578,7 @@ function Poll({
|
||||||
minExpiration,
|
minExpiration,
|
||||||
maxCharactersPerOption,
|
maxCharactersPerOption,
|
||||||
}) {
|
}) {
|
||||||
|
const { _ } = useLingui();
|
||||||
const { options, expiresIn, multiple } = poll;
|
const { options, expiresIn, multiple } = poll;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -2534,7 +2592,7 @@ function Poll({
|
||||||
value={option}
|
value={option}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
maxlength={maxCharactersPerOption}
|
maxlength={maxCharactersPerOption}
|
||||||
placeholder={`Choice ${i + 1}`}
|
placeholder={t`Choice ${i + 1}`}
|
||||||
lang={lang}
|
lang={lang}
|
||||||
spellCheck="true"
|
spellCheck="true"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
|
@ -2553,7 +2611,7 @@ function Poll({
|
||||||
onInput(poll);
|
onInput(poll);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon icon="x" size="s" />
|
<Icon icon="x" size="s" alt={t`Remove`} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
@ -2581,10 +2639,10 @@ function Poll({
|
||||||
onInput(poll);
|
onInput(poll);
|
||||||
}}
|
}}
|
||||||
/>{' '}
|
/>{' '}
|
||||||
Multiple choices
|
<Trans>Multiple choices</Trans>
|
||||||
</label>
|
</label>
|
||||||
<label class="expires-in">
|
<label class="expires-in">
|
||||||
Duration{' '}
|
<Trans>Duration</Trans>{' '}
|
||||||
<select
|
<select
|
||||||
value={expiresIn}
|
value={expiresIn}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
@ -2595,12 +2653,12 @@ function Poll({
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{Object.entries(expiryOptions)
|
{Object.entries(expiryOptions)
|
||||||
.filter(([label, value]) => {
|
.filter(([value]) => {
|
||||||
return value >= minExpiration && value <= maxExpiration;
|
return value >= minExpiration && value <= maxExpiration;
|
||||||
})
|
})
|
||||||
.map(([label, value]) => (
|
.map(([value, label]) => (
|
||||||
<option value={value} key={value}>
|
<option value={value} key={value}>
|
||||||
{label}
|
{label()}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
|
@ -2615,7 +2673,7 @@ function Poll({
|
||||||
onInput(null);
|
onInput(null);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Remove poll
|
<Trans>Remove poll</Trans>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -2812,7 +2870,7 @@ function MentionModal({
|
||||||
<div id="mention-sheet" class="sheet">
|
<div id="mention-sheet" class="sheet">
|
||||||
{!!onClose && (
|
{!!onClose && (
|
||||||
<button type="button" class="sheet-close" onClick={onClose}>
|
<button type="button" class="sheet-close" onClick={onClose}>
|
||||||
<Icon icon="x" />
|
<Icon icon="x" alt={t`Close`} />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<header>
|
<header>
|
||||||
|
@ -2829,7 +2887,7 @@ function MentionModal({
|
||||||
required
|
required
|
||||||
type="search"
|
type="search"
|
||||||
class="block"
|
class="block"
|
||||||
placeholder="Search accounts"
|
placeholder={t`Search accounts`}
|
||||||
onInput={(e) => {
|
onInput={(e) => {
|
||||||
const { value } = e.target;
|
const { value } = e.target;
|
||||||
debouncedLoadAccounts(value);
|
debouncedLoadAccounts(value);
|
||||||
|
@ -2870,7 +2928,7 @@ function MentionModal({
|
||||||
selectAccount(account);
|
selectAccount(account);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon icon="plus" size="xl" />
|
<Icon icon="plus" size="xl" alt={t`Add`} />
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
|
@ -2882,7 +2940,9 @@ function MentionModal({
|
||||||
</div>
|
</div>
|
||||||
) : uiState === 'error' ? (
|
) : uiState === 'error' ? (
|
||||||
<div class="ui-state">
|
<div class="ui-state">
|
||||||
<p>Error loading accounts</p>
|
<p>
|
||||||
|
<Trans>Error loading accounts</Trans>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</main>
|
</main>
|
||||||
|
@ -3018,12 +3078,14 @@ function CustomEmojisModal({
|
||||||
<div id="custom-emojis-sheet" class="sheet">
|
<div id="custom-emojis-sheet" class="sheet">
|
||||||
{!!onClose && (
|
{!!onClose && (
|
||||||
<button type="button" class="sheet-close" onClick={onClose}>
|
<button type="button" class="sheet-close" onClick={onClose}>
|
||||||
<Icon icon="x" />
|
<Icon icon="x" alt={t`Close`} />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<header>
|
<header>
|
||||||
<div>
|
<div>
|
||||||
<b>Custom emojis</b>{' '}
|
<b>
|
||||||
|
<Trans>Custom emojis</Trans>
|
||||||
|
</b>{' '}
|
||||||
{uiState === 'loading' ? (
|
{uiState === 'loading' ? (
|
||||||
<Loader />
|
<Loader />
|
||||||
) : (
|
) : (
|
||||||
|
@ -3042,7 +3104,7 @@ function CustomEmojisModal({
|
||||||
<input
|
<input
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
type="search"
|
type="search"
|
||||||
placeholder="Search emoji"
|
placeholder={t`Search emoji`}
|
||||||
onInput={onFind}
|
onInput={onFind}
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
autocorrect="off"
|
autocorrect="off"
|
||||||
|
@ -3072,7 +3134,9 @@ function CustomEmojisModal({
|
||||||
<div class="custom-emojis-list">
|
<div class="custom-emojis-list">
|
||||||
{uiState === 'error' && (
|
{uiState === 'error' && (
|
||||||
<div class="ui-state">
|
<div class="ui-state">
|
||||||
<p>Error loading custom emojis</p>
|
<p>
|
||||||
|
<Trans>Error loading custom emojis</Trans>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{uiState === 'default' &&
|
{uiState === 'default' &&
|
||||||
|
@ -3082,8 +3146,8 @@ function CustomEmojisModal({
|
||||||
<>
|
<>
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
{{
|
{{
|
||||||
'--recent--': 'Recently used',
|
'--recent--': t`Recently used`,
|
||||||
'--others--': 'Others',
|
'--others--': t`Others`,
|
||||||
}[category] || category}
|
}[category] || category}
|
||||||
</div>
|
</div>
|
||||||
<CustomEmojisList
|
<CustomEmojisList
|
||||||
|
@ -3101,6 +3165,7 @@ function CustomEmojisModal({
|
||||||
}
|
}
|
||||||
|
|
||||||
const CustomEmojisList = memo(({ emojis, onSelect }) => {
|
const CustomEmojisList = memo(({ emojis, onSelect }) => {
|
||||||
|
const { i18n } = useLingui();
|
||||||
const [max, setMax] = useState(CUSTOM_EMOJIS_COUNT);
|
const [max, setMax] = useState(CUSTOM_EMOJIS_COUNT);
|
||||||
const showMore = emojis.length > max;
|
const showMore = emojis.length > max;
|
||||||
return (
|
return (
|
||||||
|
@ -3120,7 +3185,7 @@ const CustomEmojisList = memo(({ emojis, onSelect }) => {
|
||||||
class="plain small"
|
class="plain small"
|
||||||
onClick={() => setMax(max + CUSTOM_EMOJIS_COUNT)}
|
onClick={() => setMax(max + CUSTOM_EMOJIS_COUNT)}
|
||||||
>
|
>
|
||||||
{(emojis.length - max).toLocaleString()} more…
|
<Trans>{i18n.number(emojis.length - max)} more…</Trans>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
@ -3187,6 +3252,7 @@ const CustomEmojiButton = memo(({ emoji, onClick, showCode }) => {
|
||||||
|
|
||||||
const GIFS_PER_PAGE = 20;
|
const GIFS_PER_PAGE = 20;
|
||||||
function GIFPickerModal({ onClose = () => {}, onSelect = () => {} }) {
|
function GIFPickerModal({ onClose = () => {}, onSelect = () => {} }) {
|
||||||
|
const { i18n } = useLingui();
|
||||||
const [uiState, setUIState] = useState('default');
|
const [uiState, setUIState] = useState('default');
|
||||||
const [results, setResults] = useState([]);
|
const [results, setResults] = useState([]);
|
||||||
const formRef = useRef(null);
|
const formRef = useRef(null);
|
||||||
|
@ -3212,6 +3278,7 @@ function GIFPickerModal({ onClose = () => {}, onSelect = () => {} }) {
|
||||||
limit: GIFS_PER_PAGE,
|
limit: GIFS_PER_PAGE,
|
||||||
bundle: 'messaging_non_clips',
|
bundle: 'messaging_non_clips',
|
||||||
offset,
|
offset,
|
||||||
|
lang: i18n.locale || 'en',
|
||||||
};
|
};
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
'https://api.giphy.com/v1/gifs/search?' + new URLSearchParams(query),
|
'https://api.giphy.com/v1/gifs/search?' + new URLSearchParams(query),
|
||||||
|
@ -3241,7 +3308,7 @@ function GIFPickerModal({ onClose = () => {}, onSelect = () => {} }) {
|
||||||
<div id="gif-picker-sheet" class="sheet">
|
<div id="gif-picker-sheet" class="sheet">
|
||||||
{!!onClose && (
|
{!!onClose && (
|
||||||
<button type="button" class="sheet-close" onClick={onClose}>
|
<button type="button" class="sheet-close" onClick={onClose}>
|
||||||
<Icon icon="x" />
|
<Icon icon="x" alt={t`Close`} />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<header>
|
<header>
|
||||||
|
@ -3256,7 +3323,7 @@ function GIFPickerModal({ onClose = () => {}, onSelect = () => {} }) {
|
||||||
ref={qRef}
|
ref={qRef}
|
||||||
type="search"
|
type="search"
|
||||||
name="q"
|
name="q"
|
||||||
placeholder="Search GIFs"
|
placeholder={t`Search GIFs`}
|
||||||
required
|
required
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
autocorrect="off"
|
autocorrect="off"
|
||||||
|
@ -3271,13 +3338,16 @@ function GIFPickerModal({ onClose = () => {}, onSelect = () => {} }) {
|
||||||
src={poweredByGiphyURL}
|
src={poweredByGiphyURL}
|
||||||
width="86"
|
width="86"
|
||||||
height="30"
|
height="30"
|
||||||
|
alt={t`Powered by GIPHY`}
|
||||||
/>
|
/>
|
||||||
</form>
|
</form>
|
||||||
</header>
|
</header>
|
||||||
<main ref={scrollableRef} class={uiState === 'loading' ? 'loading' : ''}>
|
<main ref={scrollableRef} class={uiState === 'loading' ? 'loading' : ''}>
|
||||||
{uiState === 'default' && (
|
{uiState === 'default' && (
|
||||||
<div class="ui-state">
|
<div class="ui-state">
|
||||||
<p class="insignificant">Type to search GIFs</p>
|
<p class="insignificant">
|
||||||
|
<Trans>Type to search GIFs</Trans>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{uiState === 'loading' && !results?.data?.length && (
|
{uiState === 'loading' && !results?.data?.length && (
|
||||||
|
@ -3373,7 +3443,9 @@ function GIFPickerModal({ onClose = () => {}, onSelect = () => {} }) {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon icon="chevron-left" />
|
<Icon icon="chevron-left" />
|
||||||
<span>Previous</span>
|
<span>
|
||||||
|
<Trans>Previous</Trans>
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<span />
|
<span />
|
||||||
|
@ -3389,7 +3461,10 @@ function GIFPickerModal({ onClose = () => {}, onSelect = () => {} }) {
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span>Next</span> <Icon icon="chevron-right" />
|
<span>
|
||||||
|
<Trans>Next</Trans>
|
||||||
|
</span>{' '}
|
||||||
|
<Icon icon="chevron-right" />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
|
@ -3403,7 +3478,9 @@ function GIFPickerModal({ onClose = () => {}, onSelect = () => {} }) {
|
||||||
)}
|
)}
|
||||||
{uiState === 'error' && (
|
{uiState === 'error' && (
|
||||||
<div class="ui-state">
|
<div class="ui-state">
|
||||||
<p>Error loading GIFs</p>
|
<p>
|
||||||
|
<Trans>Error loading GIFs</Trans>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</main>
|
</main>
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import './drafts.css';
|
import './drafts.css';
|
||||||
|
|
||||||
|
import { t, Trans } from '@lingui/macro';
|
||||||
import { useEffect, useMemo, useReducer, useState } from 'react';
|
import { useEffect, useMemo, useReducer, useState } from 'react';
|
||||||
|
|
||||||
import { api } from '../utils/api';
|
import { api } from '../utils/api';
|
||||||
|
@ -54,17 +55,20 @@ function Drafts({ onClose }) {
|
||||||
<div class="sheet">
|
<div class="sheet">
|
||||||
{!!onClose && (
|
{!!onClose && (
|
||||||
<button type="button" class="sheet-close" onClick={onClose}>
|
<button type="button" class="sheet-close" onClick={onClose}>
|
||||||
<Icon icon="x" />
|
<Icon icon="x" alt={t`Close`} />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<header>
|
<header>
|
||||||
<h2>
|
<h2>
|
||||||
Unsent drafts <Loader abrupt hidden={uiState !== 'loading'} />
|
<Trans>Unsent drafts</Trans>{' '}
|
||||||
|
<Loader abrupt hidden={uiState !== 'loading'} />
|
||||||
</h2>
|
</h2>
|
||||||
{hasDrafts && (
|
{hasDrafts && (
|
||||||
<div class="insignificant">
|
<div class="insignificant">
|
||||||
|
<Trans>
|
||||||
Looks like you have unsent drafts. Let's continue where you left
|
Looks like you have unsent drafts. Let's continue where you left
|
||||||
off.
|
off.
|
||||||
|
</Trans>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</header>
|
</header>
|
||||||
|
@ -91,7 +95,11 @@ function Drafts({ onClose }) {
|
||||||
</time>
|
</time>
|
||||||
</b>
|
</b>
|
||||||
<MenuConfirm
|
<MenuConfirm
|
||||||
confirmLabel={<span>Delete this draft?</span>}
|
confirmLabel={
|
||||||
|
<span>
|
||||||
|
<Trans>Delete this draft?</Trans>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
menuItemClassName="danger"
|
menuItemClassName="danger"
|
||||||
align="end"
|
align="end"
|
||||||
disabled={uiState === 'loading'}
|
disabled={uiState === 'loading'}
|
||||||
|
@ -104,7 +112,7 @@ function Drafts({ onClose }) {
|
||||||
reload();
|
reload();
|
||||||
// }
|
// }
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
alert('Error deleting draft! Please try again.');
|
alert(t`Error deleting draft! Please try again.`);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
}}
|
}}
|
||||||
|
@ -114,7 +122,7 @@ function Drafts({ onClose }) {
|
||||||
class="small light"
|
class="small light"
|
||||||
disabled={uiState === 'loading'}
|
disabled={uiState === 'loading'}
|
||||||
>
|
>
|
||||||
Delete…
|
<Trans>Delete…</Trans>
|
||||||
</button>
|
</button>
|
||||||
</MenuConfirm>
|
</MenuConfirm>
|
||||||
</div>
|
</div>
|
||||||
|
@ -133,7 +141,7 @@ function Drafts({ onClose }) {
|
||||||
.fetch();
|
.fetch();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
alert('Error fetching reply-to status!');
|
alert(t`Error fetching reply-to status!`);
|
||||||
setUIState('default');
|
setUIState('default');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -156,7 +164,11 @@ function Drafts({ onClose }) {
|
||||||
{drafts.length > 1 && (
|
{drafts.length > 1 && (
|
||||||
<p>
|
<p>
|
||||||
<MenuConfirm
|
<MenuConfirm
|
||||||
confirmLabel={<span>Delete all drafts?</span>}
|
confirmLabel={
|
||||||
|
<span>
|
||||||
|
<Trans>Delete all drafts?</Trans>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
menuItemClassName="danger"
|
menuItemClassName="danger"
|
||||||
disabled={uiState === 'loading'}
|
disabled={uiState === 'loading'}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
@ -172,7 +184,7 @@ function Drafts({ onClose }) {
|
||||||
reload();
|
reload();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
alert('Error deleting drafts! Please try again.');
|
alert(t`Error deleting drafts! Please try again.`);
|
||||||
setUIState('error');
|
setUIState('error');
|
||||||
}
|
}
|
||||||
// }
|
// }
|
||||||
|
@ -184,14 +196,16 @@ function Drafts({ onClose }) {
|
||||||
class="light danger"
|
class="light danger"
|
||||||
disabled={uiState === 'loading'}
|
disabled={uiState === 'loading'}
|
||||||
>
|
>
|
||||||
Delete all…
|
<Trans>Delete all…</Trans>
|
||||||
</button>
|
</button>
|
||||||
</MenuConfirm>
|
</MenuConfirm>
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<p>No drafts found.</p>
|
<p>
|
||||||
|
<Trans>No drafts found.</Trans>
|
||||||
|
</p>
|
||||||
)}
|
)}
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
@ -226,10 +240,10 @@ function MiniDraft({ draft }) {
|
||||||
: {}
|
: {}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{hasPoll && <Icon icon="poll" />}
|
{hasPoll && <Icon icon="poll" alt={t`Poll`} />}
|
||||||
{hasMedia && (
|
{hasMedia && (
|
||||||
<span>
|
<span>
|
||||||
<Icon icon="attachment" />{' '}
|
<Icon icon="attachment" alt={t`Media`} />{' '}
|
||||||
<small>{mediaAttachments?.length}</small>
|
<small>{mediaAttachments?.length}</small>
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import './embed-modal.css';
|
import './embed-modal.css';
|
||||||
|
|
||||||
|
import { t, Trans } from '@lingui/macro';
|
||||||
|
|
||||||
import Icon from './icon';
|
import Icon from './icon';
|
||||||
|
|
||||||
function EmbedModal({ html, url, width, height, onClose = () => {} }) {
|
function EmbedModal({ html, url, width, height, onClose = () => {} }) {
|
||||||
|
@ -7,7 +9,7 @@ function EmbedModal({ html, url, width, height, onClose = () => {} }) {
|
||||||
<div class="embed-modal-container">
|
<div class="embed-modal-container">
|
||||||
<div class="top-controls">
|
<div class="top-controls">
|
||||||
<button type="button" class="light" onClick={() => onClose()}>
|
<button type="button" class="light" onClick={() => onClose()}>
|
||||||
<Icon icon="x" />
|
<Icon icon="x" alt={t`Close`} />
|
||||||
</button>
|
</button>
|
||||||
{url && (
|
{url && (
|
||||||
<a
|
<a
|
||||||
|
@ -16,7 +18,10 @@ function EmbedModal({ html, url, width, height, onClose = () => {} }) {
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
class="button plain"
|
class="button plain"
|
||||||
>
|
>
|
||||||
<span>Open link</span> <Icon icon="external" />
|
<span>
|
||||||
|
<Trans>Open in new window</Trans>
|
||||||
|
</span>{' '}
|
||||||
|
<Icon icon="external" />
|
||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { t, Trans } from '@lingui/macro';
|
||||||
import { useState } from 'preact/hooks';
|
import { useState } from 'preact/hooks';
|
||||||
|
|
||||||
import { api } from '../utils/api';
|
import { api } from '../utils/api';
|
||||||
|
@ -38,7 +39,7 @@ function FollowRequestButtons({ accountID, onChange }) {
|
||||||
})();
|
})();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Accept
|
<Trans>Accept</Trans>
|
||||||
</button>{' '}
|
</button>{' '}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
@ -64,14 +65,18 @@ function FollowRequestButtons({ accountID, onChange }) {
|
||||||
})();
|
})();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Reject
|
<Trans>Reject</Trans>
|
||||||
</button>
|
</button>
|
||||||
<span class="follow-request-states">
|
<span class="follow-request-states">
|
||||||
{hasRelationship && requestState ? (
|
{hasRelationship && requestState ? (
|
||||||
requestState === 'accept' ? (
|
requestState === 'accept' ? (
|
||||||
<Icon icon="check-circle" alt="Accepted" class="follow-accepted" />
|
<Icon
|
||||||
|
icon="check-circle"
|
||||||
|
alt={t`Accepted`}
|
||||||
|
class="follow-accepted"
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Icon icon="x-circle" alt="Rejected" class="follow-rejected" />
|
<Icon icon="x-circle" alt={t`Rejected`} class="follow-rejected" />
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
<Loader hidden={uiState !== 'loading'} />
|
<Loader hidden={uiState !== 'loading'} />
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import './generic-accounts.css';
|
import './generic-accounts.css';
|
||||||
|
|
||||||
|
import { t, Trans } from '@lingui/macro';
|
||||||
import { useEffect, useRef, useState } from 'preact/hooks';
|
import { useEffect, useRef, useState } from 'preact/hooks';
|
||||||
import { InView } from 'react-intersection-observer';
|
import { InView } from 'react-intersection-observer';
|
||||||
import { useSnapshot } from 'valtio';
|
import { useSnapshot } from 'valtio';
|
||||||
|
@ -20,7 +21,7 @@ export default function GenericAccounts({
|
||||||
excludeRelationshipAttrs = [],
|
excludeRelationshipAttrs = [],
|
||||||
postID,
|
postID,
|
||||||
onClose = () => {},
|
onClose = () => {},
|
||||||
blankCopy = 'Nothing to show',
|
blankCopy = t`Nothing to show`,
|
||||||
}) {
|
}) {
|
||||||
const { masto, instance: currentInstance } = api();
|
const { masto, instance: currentInstance } = api();
|
||||||
const isCurrentInstance = instance ? instance === currentInstance : true;
|
const isCurrentInstance = instance ? instance === currentInstance : true;
|
||||||
|
@ -138,10 +139,10 @@ export default function GenericAccounts({
|
||||||
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}>
|
||||||
<Icon icon="x" />
|
<Icon icon="x" alt={t`Close`} />
|
||||||
</button>
|
</button>
|
||||||
<header>
|
<header>
|
||||||
<h2>{heading || 'Accounts'}</h2>
|
<h2>{heading || t`Accounts`}</h2>
|
||||||
</header>
|
</header>
|
||||||
<main>
|
<main>
|
||||||
{post && (
|
{post && (
|
||||||
|
@ -201,11 +202,13 @@ export default function GenericAccounts({
|
||||||
class="plain block"
|
class="plain block"
|
||||||
onClick={() => loadAccounts()}
|
onClick={() => loadAccounts()}
|
||||||
>
|
>
|
||||||
Show more…
|
<Trans>Show more…</Trans>
|
||||||
</button>
|
</button>
|
||||||
</InView>
|
</InView>
|
||||||
) : (
|
) : (
|
||||||
<p class="ui-state insignificant">The end.</p>
|
<p class="ui-state insignificant">
|
||||||
|
<Trans>The end.</Trans>
|
||||||
|
</p>
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
uiState === 'loading' && (
|
uiState === 'loading' && (
|
||||||
|
@ -220,7 +223,9 @@ export default function GenericAccounts({
|
||||||
<Loader abrupt />
|
<Loader abrupt />
|
||||||
</p>
|
</p>
|
||||||
) : uiState === 'error' ? (
|
) : uiState === 'error' ? (
|
||||||
<p class="ui-state">Error loading accounts</p>
|
<p class="ui-state">
|
||||||
|
<Trans>Error loading accounts</Trans>
|
||||||
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<p class="ui-state insignificant">{blankCopy}</p>
|
<p class="ui-state insignificant">{blankCopy}</p>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import './keyboard-shortcuts-help.css';
|
import './keyboard-shortcuts-help.css';
|
||||||
|
|
||||||
|
import { t, Trans } from '@lingui/macro';
|
||||||
import { memo } from 'preact/compat';
|
import { memo } from 'preact/compat';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
import { useSnapshot } from 'valtio';
|
import { useSnapshot } from 'valtio';
|
||||||
|
@ -35,28 +36,31 @@ export default memo(function KeyboardShortcutsHelp() {
|
||||||
<Modal onClose={onClose}>
|
<Modal onClose={onClose}>
|
||||||
<div id="keyboard-shortcuts-help-container" class="sheet" tabindex="-1">
|
<div id="keyboard-shortcuts-help-container" class="sheet" tabindex="-1">
|
||||||
<button type="button" class="sheet-close" onClick={onClose}>
|
<button type="button" class="sheet-close" onClick={onClose}>
|
||||||
<Icon icon="x" />
|
<Icon icon="x" alt={t`Close`} />
|
||||||
</button>
|
</button>
|
||||||
<header>
|
<header>
|
||||||
<h2>Keyboard shortcuts</h2>
|
<h2>
|
||||||
|
<Trans>Keyboard shortcuts</Trans>
|
||||||
|
</h2>
|
||||||
</header>
|
</header>
|
||||||
<main>
|
<main>
|
||||||
<table>
|
<table>
|
||||||
|
<tbody>
|
||||||
{[
|
{[
|
||||||
{
|
{
|
||||||
action: 'Keyboard shortcuts help',
|
action: t`Keyboard shortcuts help`,
|
||||||
keys: <kbd>?</kbd>,
|
keys: <kbd>?</kbd>,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: 'Next post',
|
action: t`Next post`,
|
||||||
keys: <kbd>j</kbd>,
|
keys: <kbd>j</kbd>,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: 'Previous post',
|
action: t`Previous post`,
|
||||||
keys: <kbd>k</kbd>,
|
keys: <kbd>k</kbd>,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: 'Skip carousel to next post',
|
action: t`Skip carousel to next post`,
|
||||||
keys: (
|
keys: (
|
||||||
<>
|
<>
|
||||||
<kbd>Shift</kbd> + <kbd>j</kbd>
|
<kbd>Shift</kbd> + <kbd>j</kbd>
|
||||||
|
@ -64,7 +68,7 @@ export default memo(function KeyboardShortcutsHelp() {
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: 'Skip carousel to previous post',
|
action: t`Skip carousel to previous post`,
|
||||||
keys: (
|
keys: (
|
||||||
<>
|
<>
|
||||||
<kbd>Shift</kbd> + <kbd>k</kbd>
|
<kbd>Shift</kbd> + <kbd>k</kbd>
|
||||||
|
@ -72,11 +76,11 @@ export default memo(function KeyboardShortcutsHelp() {
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: 'Load new posts',
|
action: t`Load new posts`,
|
||||||
keys: <kbd>.</kbd>,
|
keys: <kbd>.</kbd>,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: 'Open post details',
|
action: t`Open post details`,
|
||||||
keys: (
|
keys: (
|
||||||
<>
|
<>
|
||||||
<kbd>Enter</kbd> or <kbd>o</kbd>
|
<kbd>Enter</kbd> or <kbd>o</kbd>
|
||||||
|
@ -85,16 +89,16 @@ export default memo(function KeyboardShortcutsHelp() {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: (
|
action: (
|
||||||
<>
|
<Trans>
|
||||||
Expand content warning or
|
Expand content warning or
|
||||||
<br />
|
<br />
|
||||||
toggle expanded/collapsed thread
|
toggle expanded/collapsed thread
|
||||||
</>
|
</Trans>
|
||||||
),
|
),
|
||||||
keys: <kbd>x</kbd>,
|
keys: <kbd>x</kbd>,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: 'Close post or dialogs',
|
action: t`Close post or dialogs`,
|
||||||
keys: (
|
keys: (
|
||||||
<>
|
<>
|
||||||
<kbd>Esc</kbd> or <kbd>Backspace</kbd>
|
<kbd>Esc</kbd> or <kbd>Backspace</kbd>
|
||||||
|
@ -102,7 +106,7 @@ export default memo(function KeyboardShortcutsHelp() {
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: 'Focus column in multi-column mode',
|
action: t`Focus column in multi-column mode`,
|
||||||
keys: (
|
keys: (
|
||||||
<>
|
<>
|
||||||
<kbd>1</kbd> to <kbd>9</kbd>
|
<kbd>1</kbd> to <kbd>9</kbd>
|
||||||
|
@ -110,11 +114,11 @@ export default memo(function KeyboardShortcutsHelp() {
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: 'Compose new post',
|
action: t`Compose new post`,
|
||||||
keys: <kbd>c</kbd>,
|
keys: <kbd>c</kbd>,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: 'Compose new post (new window)',
|
action: t`Compose new post (new window)`,
|
||||||
className: 'insignificant',
|
className: 'insignificant',
|
||||||
keys: (
|
keys: (
|
||||||
<>
|
<>
|
||||||
|
@ -123,7 +127,7 @@ export default memo(function KeyboardShortcutsHelp() {
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: 'Send post',
|
action: t`Send post`,
|
||||||
keys: (
|
keys: (
|
||||||
<>
|
<>
|
||||||
<kbd>Ctrl</kbd> + <kbd>Enter</kbd> or <kbd>⌘</kbd> +{' '}
|
<kbd>Ctrl</kbd> + <kbd>Enter</kbd> or <kbd>⌘</kbd> +{' '}
|
||||||
|
@ -132,15 +136,15 @@ export default memo(function KeyboardShortcutsHelp() {
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: 'Search',
|
action: t`Search`,
|
||||||
keys: <kbd>/</kbd>,
|
keys: <kbd>/</kbd>,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: 'Reply',
|
action: t`Reply`,
|
||||||
keys: <kbd>r</kbd>,
|
keys: <kbd>r</kbd>,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: 'Reply (new window)',
|
action: t`Reply (new window)`,
|
||||||
className: 'insignificant',
|
className: 'insignificant',
|
||||||
keys: (
|
keys: (
|
||||||
<>
|
<>
|
||||||
|
@ -149,7 +153,7 @@ export default memo(function KeyboardShortcutsHelp() {
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: 'Like (favourite)',
|
action: t`Like (favourite)`,
|
||||||
keys: (
|
keys: (
|
||||||
<>
|
<>
|
||||||
<kbd>l</kbd> or <kbd>f</kbd>
|
<kbd>l</kbd> or <kbd>f</kbd>
|
||||||
|
@ -157,7 +161,7 @@ export default memo(function KeyboardShortcutsHelp() {
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: 'Boost',
|
action: t`Boost`,
|
||||||
keys: (
|
keys: (
|
||||||
<>
|
<>
|
||||||
<kbd>Shift</kbd> + <kbd>b</kbd>
|
<kbd>Shift</kbd> + <kbd>b</kbd>
|
||||||
|
@ -165,11 +169,11 @@ export default memo(function KeyboardShortcutsHelp() {
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: 'Bookmark',
|
action: t`Bookmark`,
|
||||||
keys: <kbd>d</kbd>,
|
keys: <kbd>d</kbd>,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: 'Toggle Cloak mode',
|
action: t`Toggle Cloak mode`,
|
||||||
keys: (
|
keys: (
|
||||||
<>
|
<>
|
||||||
<kbd>Shift</kbd> + <kbd>Alt</kbd> + <kbd>k</kbd>
|
<kbd>Shift</kbd> + <kbd>Alt</kbd> + <kbd>k</kbd>
|
||||||
|
@ -182,6 +186,7 @@ export default memo(function KeyboardShortcutsHelp() {
|
||||||
<td>{keys}</td>
|
<td>{keys}</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
42
src/components/lang-selector.jsx
Normal file
42
src/components/lang-selector.jsx
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
import { useLingui } from '@lingui/react';
|
||||||
|
|
||||||
|
import { activateLang, DEFAULT_LANG, LOCALES } from '../utils/lang';
|
||||||
|
import localeCode2Text from '../utils/localeCode2Text';
|
||||||
|
|
||||||
|
export default function LangSelector() {
|
||||||
|
const { i18n } = useLingui();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<label class="lang-selector">
|
||||||
|
🌐{' '}
|
||||||
|
<select
|
||||||
|
value={i18n.locale || DEFAULT_LANG}
|
||||||
|
onChange={(e) => {
|
||||||
|
localStorage.setItem('lang', e.target.value);
|
||||||
|
activateLang(e.target.value);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{LOCALES.map((lang) => {
|
||||||
|
if (lang === 'pseudo-LOCALE') {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<hr />
|
||||||
|
<option value={lang} key={lang}>
|
||||||
|
Pseudolocalization (test)
|
||||||
|
</option>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const common = localeCode2Text(lang);
|
||||||
|
const native = localeCode2Text({ code: lang, locale: lang });
|
||||||
|
const same = common === native;
|
||||||
|
return (
|
||||||
|
<option value={lang} key={lang}>
|
||||||
|
{same ? common : `${common} (${native})`}
|
||||||
|
</option>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { t, Trans } from '@lingui/macro';
|
||||||
import { useEffect, useRef, useState } from 'preact/hooks';
|
import { useEffect, useRef, useState } from 'preact/hooks';
|
||||||
|
|
||||||
import { api } from '../utils/api';
|
import { api } from '../utils/api';
|
||||||
|
@ -29,11 +30,11 @@ function ListAddEdit({ list, onClose }) {
|
||||||
<div class="sheet">
|
<div class="sheet">
|
||||||
{!!onClose && (
|
{!!onClose && (
|
||||||
<button type="button" class="sheet-close" onClick={onClose}>
|
<button type="button" class="sheet-close" onClick={onClose}>
|
||||||
<Icon icon="x" />
|
<Icon icon="x" alt={t`Close`} />
|
||||||
</button>
|
</button>
|
||||||
)}{' '}
|
)}{' '}
|
||||||
<header>
|
<header>
|
||||||
<h2>{editMode ? 'Edit list' : 'New list'}</h2>
|
<h2>{editMode ? t`Edit list` : t`New list`}</h2>
|
||||||
</header>
|
</header>
|
||||||
<main>
|
<main>
|
||||||
<form
|
<form
|
||||||
|
@ -88,7 +89,9 @@ function ListAddEdit({ list, onClose }) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
setUIState('error');
|
setUIState('error');
|
||||||
alert(
|
alert(
|
||||||
editMode ? 'Unable to edit list.' : 'Unable to create list.',
|
editMode
|
||||||
|
? t`Unable to edit list.`
|
||||||
|
: t`Unable to create list.`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
@ -96,7 +99,7 @@ function ListAddEdit({ list, onClose }) {
|
||||||
>
|
>
|
||||||
<div class="list-form-row">
|
<div class="list-form-row">
|
||||||
<label for="list-title">
|
<label for="list-title">
|
||||||
Name{' '}
|
<Trans>Name</Trans>{' '}
|
||||||
<input
|
<input
|
||||||
ref={nameFieldRef}
|
ref={nameFieldRef}
|
||||||
type="text"
|
type="text"
|
||||||
|
@ -115,9 +118,15 @@ function ListAddEdit({ list, onClose }) {
|
||||||
required
|
required
|
||||||
disabled={uiState === 'loading'}
|
disabled={uiState === 'loading'}
|
||||||
>
|
>
|
||||||
<option value="list">Show replies to list members</option>
|
<option value="list">
|
||||||
<option value="followed">Show replies to people I follow</option>
|
<Trans>Show replies to list members</Trans>
|
||||||
<option value="none">Don't show replies</option>
|
</option>
|
||||||
|
<option value="followed">
|
||||||
|
<Trans>Show replies to people I follow</Trans>
|
||||||
|
</option>
|
||||||
|
<option value="none">
|
||||||
|
<Trans>Don't show replies</Trans>
|
||||||
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
{supportsExclusive && (
|
{supportsExclusive && (
|
||||||
|
@ -129,20 +138,20 @@ function ListAddEdit({ list, onClose }) {
|
||||||
name="exclusive"
|
name="exclusive"
|
||||||
disabled={uiState === 'loading'}
|
disabled={uiState === 'loading'}
|
||||||
/>{' '}
|
/>{' '}
|
||||||
Hide posts on this list from Home/Following
|
<Trans>Hide posts on this list from Home/Following</Trans>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div class="list-form-footer">
|
<div class="list-form-footer">
|
||||||
<button type="submit" disabled={uiState === 'loading'}>
|
<button type="submit" disabled={uiState === 'loading'}>
|
||||||
{editMode ? 'Save' : 'Create'}
|
{editMode ? t`Save` : t`Create`}
|
||||||
</button>
|
</button>
|
||||||
{editMode && (
|
{editMode && (
|
||||||
<MenuConfirm
|
<MenuConfirm
|
||||||
disabled={uiState === 'loading'}
|
disabled={uiState === 'loading'}
|
||||||
align="end"
|
align="end"
|
||||||
menuItemClassName="danger"
|
menuItemClassName="danger"
|
||||||
confirmLabel="Delete this list?"
|
confirmLabel={t`Delete this list?`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
// const yes = confirm('Delete this list?');
|
// const yes = confirm('Delete this list?');
|
||||||
// if (!yes) return;
|
// if (!yes) return;
|
||||||
|
@ -161,7 +170,7 @@ function ListAddEdit({ list, onClose }) {
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
setUIState('error');
|
setUIState('error');
|
||||||
alert('Unable to delete list.');
|
alert(t`Unable to delete list.`);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
}}
|
}}
|
||||||
|
@ -171,7 +180,7 @@ function ListAddEdit({ list, onClose }) {
|
||||||
class="light danger"
|
class="light danger"
|
||||||
disabled={uiState === 'loading'}
|
disabled={uiState === 'loading'}
|
||||||
>
|
>
|
||||||
Delete…
|
<Trans>Delete…</Trans>
|
||||||
</button>
|
</button>
|
||||||
</MenuConfirm>
|
</MenuConfirm>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { t, Trans } from '@lingui/macro';
|
||||||
import { Menu, MenuItem } from '@szhsin/react-menu';
|
import { Menu, MenuItem } from '@szhsin/react-menu';
|
||||||
import { useState } from 'preact/hooks';
|
import { useState } from 'preact/hooks';
|
||||||
import { useSnapshot } from 'valtio';
|
import { useSnapshot } from 'valtio';
|
||||||
|
@ -29,17 +30,19 @@ export default function MediaAltModal({ alt, lang, onClose }) {
|
||||||
<div class="sheet" tabindex="-1">
|
<div class="sheet" tabindex="-1">
|
||||||
{!!onClose && (
|
{!!onClose && (
|
||||||
<button type="button" class="sheet-close outer" onClick={onClose}>
|
<button type="button" class="sheet-close outer" onClick={onClose}>
|
||||||
<Icon icon="x" />
|
<Icon icon="x" alt={t`Close`} />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<header class="header-grid">
|
<header class="header-grid">
|
||||||
<h2>Media description</h2>
|
<h2>
|
||||||
|
<Trans>Media description</Trans>
|
||||||
|
</h2>
|
||||||
<div class="header-side">
|
<div class="header-side">
|
||||||
<Menu2
|
<Menu2
|
||||||
align="end"
|
align="end"
|
||||||
menuButton={
|
menuButton={
|
||||||
<button type="button" class="plain4">
|
<button type="button" class="plain4">
|
||||||
<Icon icon="more" alt="More" size="xl" />
|
<Icon icon="more" alt={t`More`} size="xl" />
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
@ -50,7 +53,9 @@ export default function MediaAltModal({ alt, lang, onClose }) {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon icon="translate" />
|
<Icon icon="translate" />
|
||||||
<span>Translate</span>
|
<span>
|
||||||
|
<Trans>Translate</Trans>
|
||||||
|
</span>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
{supportsTTS && (
|
{supportsTTS && (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
|
@ -59,7 +64,9 @@ export default function MediaAltModal({ alt, lang, onClose }) {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon icon="speak" />
|
<Icon icon="speak" />
|
||||||
<span>Speak</span>
|
<span>
|
||||||
|
<Trans>Speak</Trans>
|
||||||
|
</span>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
)}
|
)}
|
||||||
</Menu2>
|
</Menu2>
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { t, Trans } from '@lingui/macro';
|
||||||
import { MenuDivider, MenuItem } from '@szhsin/react-menu';
|
import { MenuDivider, MenuItem } from '@szhsin/react-menu';
|
||||||
import { getBlurHashAverageColor } from 'fast-blurhash';
|
import { getBlurHashAverageColor } from 'fast-blurhash';
|
||||||
import {
|
import {
|
||||||
|
@ -243,7 +244,7 @@ function MediaModal({
|
||||||
class="carousel-button"
|
class="carousel-button"
|
||||||
onClick={() => onClose()}
|
onClick={() => onClose()}
|
||||||
>
|
>
|
||||||
<Icon icon="x" />
|
<Icon icon="x" alt={t`Close`} />
|
||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
{mediaAttachments?.length > 1 ? (
|
{mediaAttachments?.length > 1 ? (
|
||||||
|
@ -257,15 +258,13 @@ function MediaModal({
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
carouselRef.current.scrollTo({
|
const left =
|
||||||
left:
|
carouselRef.current.clientWidth * i * (isRTL() ? -1 : 1);
|
||||||
carouselRef.current.clientWidth * i * (isRTL() ? -1 : 1),
|
carouselRef.current.scrollTo({ left, behavior: 'smooth' });
|
||||||
behavior: 'smooth',
|
|
||||||
});
|
|
||||||
carouselRef.current.focus();
|
carouselRef.current.focus();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon icon="round" size="s" />
|
<Icon icon="round" size="s" alt="⸱" />
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</span>
|
</span>
|
||||||
|
@ -281,7 +280,7 @@ function MediaModal({
|
||||||
menuClassName="glass-menu"
|
menuClassName="glass-menu"
|
||||||
menuButton={
|
menuButton={
|
||||||
<button type="button" class="carousel-button">
|
<button type="button" class="carousel-button">
|
||||||
<Icon icon="more" alt="More" />
|
<Icon icon="more" alt={t`More`} />
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
@ -292,10 +291,12 @@ function MediaModal({
|
||||||
}
|
}
|
||||||
class="carousel-button"
|
class="carousel-button"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
title="Open original media in new window"
|
title={t`Open original media in new window`}
|
||||||
>
|
>
|
||||||
<Icon icon="popout" />
|
<Icon icon="popout" />
|
||||||
<span>Open original media</span>
|
<span>
|
||||||
|
<Trans>Open original media</Trans>
|
||||||
|
</span>
|
||||||
</MenuLink>
|
</MenuLink>
|
||||||
{import.meta.env.DEV && // Only dev for now
|
{import.meta.env.DEV && // Only dev for now
|
||||||
!!states.settings.mediaAltGenerator &&
|
!!states.settings.mediaAltGenerator &&
|
||||||
|
@ -310,7 +311,7 @@ function MediaModal({
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setUIState('loading');
|
setUIState('loading');
|
||||||
toastRef.current = showToast({
|
toastRef.current = showToast({
|
||||||
text: 'Attempting to describe image. Please wait...',
|
text: t`Attempting to describe image. Please wait...`,
|
||||||
duration: -1,
|
duration: -1,
|
||||||
});
|
});
|
||||||
(async function () {
|
(async function () {
|
||||||
|
@ -325,7 +326,7 @@ function MediaModal({
|
||||||
};
|
};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
showToast('Failed to describe image');
|
showToast(t`Failed to describe image`);
|
||||||
} finally {
|
} finally {
|
||||||
setUIState('default');
|
setUIState('default');
|
||||||
toastRef.current?.hideToast?.();
|
toastRef.current?.hideToast?.();
|
||||||
|
@ -334,7 +335,9 @@ function MediaModal({
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon icon="sparkles2" />
|
<Icon icon="sparkles2" />
|
||||||
<span>Describe image…</span>
|
<span>
|
||||||
|
<Trans>Describe image…</Trans>
|
||||||
|
</span>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
@ -355,7 +358,10 @@ function MediaModal({
|
||||||
// }
|
// }
|
||||||
// }}
|
// }}
|
||||||
>
|
>
|
||||||
<span class="button-label">View post </span>»
|
<span class="button-label">
|
||||||
|
<Trans>View post</Trans>{' '}
|
||||||
|
</span>
|
||||||
|
»
|
||||||
</Link>
|
</Link>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
@ -378,7 +384,7 @@ function MediaModal({
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon icon="arrow-left" />
|
<Icon icon="arrow-left" alt={t`Previous`} />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
@ -397,7 +403,7 @@ function MediaModal({
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon icon="arrow-right" />
|
<Icon icon="arrow-right" alt={t`Next`} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import './media-post.css';
|
import './media-post.css';
|
||||||
|
|
||||||
|
import { t, Trans } from '@lingui/macro';
|
||||||
import { memo } from 'preact/compat';
|
import { memo } from 'preact/compat';
|
||||||
import { useContext, useMemo } from 'preact/hooks';
|
import { useContext, useMemo } from 'preact/hooks';
|
||||||
import { useSnapshot } from 'valtio';
|
import { useSnapshot } from 'valtio';
|
||||||
|
@ -123,11 +124,13 @@ function MediaPost({
|
||||||
onMouseEnter={debugHover}
|
onMouseEnter={debugHover}
|
||||||
key={mediaKey}
|
key={mediaKey}
|
||||||
data-spoiler-text={
|
data-spoiler-text={
|
||||||
spoilerText || (sensitive ? 'Sensitive media' : undefined)
|
spoilerText || (sensitive ? t`Sensitive media` : undefined)
|
||||||
}
|
}
|
||||||
data-filtered-text={
|
data-filtered-text={
|
||||||
filterInfo
|
filterInfo
|
||||||
? `Filtered${filterTitleStr ? `: ${filterTitleStr}` : ''}`
|
? filterTitleStr
|
||||||
|
? t`Filtered: ${filterTitleStr}`
|
||||||
|
: t`Filtered`
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
class={`
|
class={`
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { t, Trans } from '@lingui/macro';
|
||||||
import { getBlurHashAverageColor } from 'fast-blurhash';
|
import { getBlurHashAverageColor } from 'fast-blurhash';
|
||||||
import { Fragment } from 'preact';
|
import { Fragment } from 'preact';
|
||||||
import { memo } from 'preact/compat';
|
import { memo } from 'preact/compat';
|
||||||
|
@ -46,7 +47,7 @@ const AltBadge = (props) => {
|
||||||
lang,
|
lang,
|
||||||
};
|
};
|
||||||
}}
|
}}
|
||||||
title="Media description"
|
title={t`Media description`}
|
||||||
>
|
>
|
||||||
{dataAltLabel}
|
{dataAltLabel}
|
||||||
{!!index && <sup>{index}</sup>}
|
{!!index && <sup>{index}</sup>}
|
||||||
|
@ -615,7 +616,7 @@ function Media({
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<div class="media-play">
|
<div class="media-play">
|
||||||
<Icon icon="play" size="xl" />
|
<Icon icon="play" size="xl" alt="▶" />
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
@ -659,7 +660,7 @@ function Media({
|
||||||
{!showOriginal && (
|
{!showOriginal && (
|
||||||
<>
|
<>
|
||||||
<div class="media-play">
|
<div class="media-play">
|
||||||
<Icon icon="play" size="xl" />
|
<Icon icon="play" size="xl" alt="▶" />
|
||||||
</div>
|
</div>
|
||||||
{!showInlineDesc && (
|
{!showInlineDesc && (
|
||||||
<AltBadge alt={description} lang={lang} index={altIndex} />
|
<AltBadge alt={description} lang={lang} index={altIndex} />
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { t, Trans } from '@lingui/macro';
|
||||||
import { useEffect } from 'preact/hooks';
|
import { useEffect } from 'preact/hooks';
|
||||||
import { useLocation, useNavigate } from 'react-router-dom';
|
import { useLocation, useNavigate } from 'react-router-dom';
|
||||||
import { subscribe, useSnapshot } from 'valtio';
|
import { subscribe, useSnapshot } from 'valtio';
|
||||||
|
@ -68,9 +69,9 @@ export default function Modals() {
|
||||||
states.reloadStatusPage++;
|
states.reloadStatusPage++;
|
||||||
showToast({
|
showToast({
|
||||||
text: {
|
text: {
|
||||||
post: 'Post published. Check it out.',
|
post: t`Post published. Check it out.`,
|
||||||
reply: 'Reply posted. Check it out.',
|
reply: t`Reply posted. Check it out.`,
|
||||||
edit: 'Post updated. Check it out.',
|
edit: t`Post updated. Check it out.`,
|
||||||
}[type || 'post'],
|
}[type || 'post'],
|
||||||
delay: 1000,
|
delay: 1000,
|
||||||
duration: 10_000, // 10 seconds
|
duration: 10_000, // 10 seconds
|
||||||
|
|
|
@ -1,16 +1,21 @@
|
||||||
import './name-text.css';
|
import './name-text.css';
|
||||||
|
|
||||||
|
import { useLingui } from '@lingui/react';
|
||||||
import { memo } from 'preact/compat';
|
import { memo } from 'preact/compat';
|
||||||
|
|
||||||
import { api } from '../utils/api';
|
import { api } from '../utils/api';
|
||||||
|
import mem from '../utils/mem';
|
||||||
import states from '../utils/states';
|
import states from '../utils/states';
|
||||||
|
|
||||||
import Avatar from './avatar';
|
import Avatar from './avatar';
|
||||||
import EmojiText from './emoji-text';
|
import EmojiText from './emoji-text';
|
||||||
|
|
||||||
const nameCollator = new Intl.Collator('en', {
|
const nameCollator = mem(
|
||||||
|
(locale) =>
|
||||||
|
new Intl.Collator(locale || undefined, {
|
||||||
sensitivity: 'base',
|
sensitivity: 'base',
|
||||||
});
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
function NameText({
|
function NameText({
|
||||||
account,
|
account,
|
||||||
|
@ -21,6 +26,7 @@ function NameText({
|
||||||
external,
|
external,
|
||||||
onClick,
|
onClick,
|
||||||
}) {
|
}) {
|
||||||
|
const { i18n } = useLingui();
|
||||||
const {
|
const {
|
||||||
acct,
|
acct,
|
||||||
avatar,
|
avatar,
|
||||||
|
@ -51,7 +57,10 @@ function NameText({
|
||||||
(trimmedUsername === trimmedDisplayName ||
|
(trimmedUsername === trimmedDisplayName ||
|
||||||
trimmedUsername === shortenedDisplayName ||
|
trimmedUsername === shortenedDisplayName ||
|
||||||
trimmedUsername === shortenedAlphaNumericDisplayName ||
|
trimmedUsername === shortenedAlphaNumericDisplayName ||
|
||||||
nameCollator.compare(trimmedUsername, shortenedDisplayName) === 0)) ||
|
nameCollator(i18n.locale).compare(
|
||||||
|
trimmedUsername,
|
||||||
|
shortenedDisplayName,
|
||||||
|
) === 0)) ||
|
||||||
shortenedAlphaNumericDisplayName === acct.toLowerCase();
|
shortenedAlphaNumericDisplayName === acct.toLowerCase();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import './nav-menu.css';
|
import './nav-menu.css';
|
||||||
|
|
||||||
|
import { t, Trans } from '@lingui/macro';
|
||||||
import { ControlledMenu, MenuDivider, MenuItem } from '@szhsin/react-menu';
|
import { ControlledMenu, MenuDivider, MenuItem } from '@szhsin/react-menu';
|
||||||
import { memo } from 'preact/compat';
|
import { memo } from 'preact/compat';
|
||||||
import { useEffect, useMemo, useRef, useState } from 'preact/hooks';
|
import { useEffect, useMemo, useRef, useState } from 'preact/hooks';
|
||||||
|
@ -122,7 +123,7 @@ function NavMenu(props) {
|
||||||
squircle={currentAccount?.info?.bot}
|
squircle={currentAccount?.info?.bot}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Icon icon="menu" size={moreThanOneAccount ? 's' : 'l'} />
|
<Icon icon="menu" size={moreThanOneAccount ? 's' : 'l'} alt={t`Menu`} />
|
||||||
</button>
|
</button>
|
||||||
<ControlledMenu
|
<ControlledMenu
|
||||||
menuClassName="nav-menu"
|
menuClassName="nav-menu"
|
||||||
|
@ -158,7 +159,7 @@ function NavMenu(props) {
|
||||||
<div class="top-menu">
|
<div class="top-menu">
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const yes = confirm('Reload page now to update?');
|
const yes = confirm(t`Reload page now to update?`);
|
||||||
if (yes) {
|
if (yes) {
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
|
@ -169,35 +170,51 @@ function NavMenu(props) {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon icon="sparkles" class="sparkle-icon" size="l" />{' '}
|
<Icon icon="sparkles" class="sparkle-icon" size="l" />{' '}
|
||||||
<span>New update available…</span>
|
<span>
|
||||||
|
<Trans>New update available…</Trans>
|
||||||
|
</span>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuDivider />
|
<MenuDivider />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<section>
|
<section>
|
||||||
<MenuLink to="/">
|
<MenuLink to="/">
|
||||||
<Icon icon="home" size="l" /> <span>Home</span>
|
<Icon icon="home" size="l" />{' '}
|
||||||
|
<span>
|
||||||
|
<Trans>Home</Trans>
|
||||||
|
</span>
|
||||||
</MenuLink>
|
</MenuLink>
|
||||||
{authenticated ? (
|
{authenticated ? (
|
||||||
<>
|
<>
|
||||||
{showFollowing && (
|
{showFollowing && (
|
||||||
<MenuLink to="/following">
|
<MenuLink to="/following">
|
||||||
<Icon icon="following" size="l" /> <span>Following</span>
|
<Icon icon="following" size="l" />{' '}
|
||||||
|
<span>
|
||||||
|
<Trans>Following</Trans>
|
||||||
|
</span>
|
||||||
</MenuLink>
|
</MenuLink>
|
||||||
)}
|
)}
|
||||||
<MenuLink to="/catchup">
|
<MenuLink to="/catchup">
|
||||||
<Icon icon="history2" size="l" />
|
<Icon icon="history2" size="l" />
|
||||||
<span>Catch-up</span>
|
<span>
|
||||||
|
<Trans>Catch-up</Trans>
|
||||||
|
</span>
|
||||||
</MenuLink>
|
</MenuLink>
|
||||||
{supports('@mastodon/mentions') && (
|
{supports('@mastodon/mentions') && (
|
||||||
<MenuLink to="/mentions">
|
<MenuLink to="/mentions">
|
||||||
<Icon icon="at" size="l" /> <span>Mentions</span>
|
<Icon icon="at" size="l" />{' '}
|
||||||
|
<span>
|
||||||
|
<Trans>Mentions</Trans>
|
||||||
|
</span>
|
||||||
</MenuLink>
|
</MenuLink>
|
||||||
)}
|
)}
|
||||||
<MenuLink to="/notifications">
|
<MenuLink to="/notifications">
|
||||||
<Icon icon="notification" size="l" /> <span>Notifications</span>
|
<Icon icon="notification" size="l" />{' '}
|
||||||
|
<span>
|
||||||
|
<Trans>Notifications</Trans>
|
||||||
|
</span>
|
||||||
{snapStates.notificationsShowNew && (
|
{snapStates.notificationsShowNew && (
|
||||||
<sup title="New" style={{ opacity: 0.5 }}>
|
<sup title={t`New`} style={{ opacity: 0.5 }}>
|
||||||
{' '}
|
{' '}
|
||||||
•
|
•
|
||||||
</sup>
|
</sup>
|
||||||
|
@ -206,7 +223,10 @@ function NavMenu(props) {
|
||||||
<MenuDivider />
|
<MenuDivider />
|
||||||
{currentAccount?.info?.id && (
|
{currentAccount?.info?.id && (
|
||||||
<MenuLink to={`/${instance}/a/${currentAccount.info.id}`}>
|
<MenuLink to={`/${instance}/a/${currentAccount.info.id}`}>
|
||||||
<Icon icon="user" size="l" /> <span>Profile</span>
|
<Icon icon="user" size="l" />{' '}
|
||||||
|
<span>
|
||||||
|
<Trans>Profile</Trans>
|
||||||
|
</span>
|
||||||
</MenuLink>
|
</MenuLink>
|
||||||
)}
|
)}
|
||||||
{lists?.length > 0 ? (
|
{lists?.length > 0 ? (
|
||||||
|
@ -217,13 +237,17 @@ function NavMenu(props) {
|
||||||
label={
|
label={
|
||||||
<>
|
<>
|
||||||
<Icon icon="list" size="l" />
|
<Icon icon="list" size="l" />
|
||||||
<span class="menu-grow">Lists</span>
|
<span class="menu-grow">
|
||||||
|
<Trans>Lists</Trans>
|
||||||
|
</span>
|
||||||
<Icon icon="chevron-right" />
|
<Icon icon="chevron-right" />
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<MenuLink to="/l">
|
<MenuLink to="/l">
|
||||||
<span>All Lists</span>
|
<span>
|
||||||
|
<Trans>All Lists</Trans>
|
||||||
|
</span>
|
||||||
</MenuLink>
|
</MenuLink>
|
||||||
{lists?.length > 0 && (
|
{lists?.length > 0 && (
|
||||||
<>
|
<>
|
||||||
|
@ -240,12 +264,17 @@ function NavMenu(props) {
|
||||||
supportsLists && (
|
supportsLists && (
|
||||||
<MenuLink to="/l">
|
<MenuLink to="/l">
|
||||||
<Icon icon="list" size="l" />
|
<Icon icon="list" size="l" />
|
||||||
<span>Lists</span>
|
<span>
|
||||||
|
<Trans>Lists</Trans>
|
||||||
|
</span>
|
||||||
</MenuLink>
|
</MenuLink>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
<MenuLink to="/b">
|
<MenuLink to="/b">
|
||||||
<Icon icon="bookmark" size="l" /> <span>Bookmarks</span>
|
<Icon icon="bookmark" size="l" />{' '}
|
||||||
|
<span>
|
||||||
|
<Trans>Bookmarks</Trans>
|
||||||
|
</span>
|
||||||
</MenuLink>
|
</MenuLink>
|
||||||
<SubMenu2
|
<SubMenu2
|
||||||
menuClassName="nav-submenu"
|
menuClassName="nav-submenu"
|
||||||
|
@ -254,49 +283,56 @@ function NavMenu(props) {
|
||||||
label={
|
label={
|
||||||
<>
|
<>
|
||||||
<Icon icon="more" size="l" />
|
<Icon icon="more" size="l" />
|
||||||
<span class="menu-grow">More…</span>
|
<span class="menu-grow">
|
||||||
|
<Trans>More…</Trans>
|
||||||
|
</span>
|
||||||
<Icon icon="chevron-right" />
|
<Icon icon="chevron-right" />
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<MenuLink to="/f">
|
<MenuLink to="/f">
|
||||||
<Icon icon="heart" size="l" /> <span>Likes</span>
|
<Icon icon="heart" size="l" />{' '}
|
||||||
|
<span>
|
||||||
|
<Trans>Likes</Trans>
|
||||||
|
</span>
|
||||||
</MenuLink>
|
</MenuLink>
|
||||||
<MenuLink to="/fh">
|
<MenuLink to="/fh">
|
||||||
<Icon icon="hashtag" size="l" />{' '}
|
<Icon icon="hashtag" size="l" />{' '}
|
||||||
<span>Followed Hashtags</span>
|
<span>
|
||||||
|
<Trans>Followed Hashtags</Trans>
|
||||||
|
</span>
|
||||||
</MenuLink>
|
</MenuLink>
|
||||||
<MenuDivider />
|
<MenuDivider />
|
||||||
{supports('@mastodon/filters') && (
|
{supports('@mastodon/filters') && (
|
||||||
<MenuLink to="/ft">
|
<MenuLink to="/ft">
|
||||||
<Icon icon="filters" size="l" />
|
<Icon icon="filters" size="l" />
|
||||||
Filters
|
<Trans>Filters</Trans>
|
||||||
</MenuLink>
|
</MenuLink>
|
||||||
)}
|
)}
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
states.showGenericAccounts = {
|
states.showGenericAccounts = {
|
||||||
id: 'mute',
|
id: 'mute',
|
||||||
heading: 'Muted users',
|
heading: t`Muted users`,
|
||||||
fetchAccounts: fetchMutes,
|
fetchAccounts: fetchMutes,
|
||||||
excludeRelationshipAttrs: ['muting'],
|
excludeRelationshipAttrs: ['muting'],
|
||||||
};
|
};
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon icon="mute" size="l" /> Muted users…
|
<Icon icon="mute" size="l" /> <Trans>Muted users…</Trans>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
states.showGenericAccounts = {
|
states.showGenericAccounts = {
|
||||||
id: 'block',
|
id: 'block',
|
||||||
heading: 'Blocked users',
|
heading: t`Blocked users`,
|
||||||
fetchAccounts: fetchBlocks,
|
fetchAccounts: fetchBlocks,
|
||||||
excludeRelationshipAttrs: ['blocking'],
|
excludeRelationshipAttrs: ['blocking'],
|
||||||
};
|
};
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon icon="block" size="l" />
|
<Icon icon="block" size="l" />
|
||||||
Blocked users…
|
<Trans>Blocked users…</Trans>
|
||||||
</MenuItem>{' '}
|
</MenuItem>{' '}
|
||||||
</SubMenu2>
|
</SubMenu2>
|
||||||
<MenuDivider />
|
<MenuDivider />
|
||||||
|
@ -305,14 +341,20 @@ function NavMenu(props) {
|
||||||
states.showAccounts = true;
|
states.showAccounts = true;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon icon="group" size="l" /> <span>Accounts…</span>
|
<Icon icon="group" size="l" />{' '}
|
||||||
|
<span>
|
||||||
|
<Trans>Accounts…</Trans>
|
||||||
|
</span>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<MenuDivider />
|
<MenuDivider />
|
||||||
<MenuLink to="/login">
|
<MenuLink to="/login">
|
||||||
<Icon icon="user" size="l" /> <span>Log in</span>
|
<Icon icon="user" size="l" />{' '}
|
||||||
|
<span>
|
||||||
|
<Trans>Log in</Trans>
|
||||||
|
</span>
|
||||||
</MenuLink>
|
</MenuLink>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
@ -320,16 +362,28 @@ function NavMenu(props) {
|
||||||
<section>
|
<section>
|
||||||
<MenuDivider />
|
<MenuDivider />
|
||||||
<MenuLink to={`/search`}>
|
<MenuLink to={`/search`}>
|
||||||
<Icon icon="search" size="l" /> <span>Search</span>
|
<Icon icon="search" size="l" />{' '}
|
||||||
|
<span>
|
||||||
|
<Trans>Search</Trans>
|
||||||
|
</span>
|
||||||
</MenuLink>
|
</MenuLink>
|
||||||
<MenuLink to={`/${instance}/trending`}>
|
<MenuLink to={`/${instance}/trending`}>
|
||||||
<Icon icon="chart" size="l" /> <span>Trending</span>
|
<Icon icon="chart" size="l" />{' '}
|
||||||
|
<span>
|
||||||
|
<Trans>Trending</Trans>
|
||||||
|
</span>
|
||||||
</MenuLink>
|
</MenuLink>
|
||||||
<MenuLink to={`/${instance}/p/l`}>
|
<MenuLink to={`/${instance}/p/l`}>
|
||||||
<Icon icon="building" size="l" /> <span>Local</span>
|
<Icon icon="building" size="l" />{' '}
|
||||||
|
<span>
|
||||||
|
<Trans>Local</Trans>
|
||||||
|
</span>
|
||||||
</MenuLink>
|
</MenuLink>
|
||||||
<MenuLink to={`/${instance}/p`}>
|
<MenuLink to={`/${instance}/p`}>
|
||||||
<Icon icon="earth" size="l" /> <span>Federated</span>
|
<Icon icon="earth" size="l" />{' '}
|
||||||
|
<span>
|
||||||
|
<Trans>Federated</Trans>
|
||||||
|
</span>
|
||||||
</MenuLink>
|
</MenuLink>
|
||||||
{authenticated ? (
|
{authenticated ? (
|
||||||
<>
|
<>
|
||||||
|
@ -340,7 +394,9 @@ function NavMenu(props) {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon icon="keyboard" size="l" />{' '}
|
<Icon icon="keyboard" size="l" />{' '}
|
||||||
<span>Keyboard shortcuts</span>
|
<span>
|
||||||
|
<Trans>Keyboard shortcuts</Trans>
|
||||||
|
</span>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
@ -348,14 +404,19 @@ function NavMenu(props) {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon icon="shortcut" size="l" />{' '}
|
<Icon icon="shortcut" size="l" />{' '}
|
||||||
<span>Shortcuts / Columns…</span>
|
<span>
|
||||||
|
<Trans>Shortcuts / Columns…</Trans>
|
||||||
|
</span>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
states.showSettings = true;
|
states.showSettings = true;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon icon="gear" size="l" /> <span>Settings…</span>
|
<Icon icon="gear" size="l" />{' '}
|
||||||
|
<span>
|
||||||
|
<Trans>Settings…</Trans>
|
||||||
|
</span>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
@ -366,7 +427,10 @@ function NavMenu(props) {
|
||||||
states.showSettings = true;
|
states.showSettings = true;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon icon="gear" size="l" /> <span>Settings…</span>
|
<Icon icon="gear" size="l" />{' '}
|
||||||
|
<span>
|
||||||
|
<Trans>Settings…</Trans>
|
||||||
|
</span>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { t, Trans } from '@lingui/macro';
|
||||||
import { memo } from 'preact/compat';
|
import { memo } from 'preact/compat';
|
||||||
import { useLayoutEffect, useState } from 'preact/hooks';
|
import { useLayoutEffect, useState } from 'preact/hooks';
|
||||||
import { useSnapshot } from 'valtio';
|
import { useSnapshot } from 'valtio';
|
||||||
|
@ -152,14 +153,18 @@ export default memo(function NotificationService() {
|
||||||
>
|
>
|
||||||
<div class="sheet" tabIndex="-1">
|
<div class="sheet" tabIndex="-1">
|
||||||
<button type="button" class="sheet-close" onClick={onClose}>
|
<button type="button" class="sheet-close" onClick={onClose}>
|
||||||
<Icon icon="x" />
|
<Icon icon="x" alt={t`Close`} />
|
||||||
</button>
|
</button>
|
||||||
<header>
|
<header>
|
||||||
<b>Notification</b>
|
<b>
|
||||||
|
<Trans>Notification</Trans>
|
||||||
|
</b>
|
||||||
</header>
|
</header>
|
||||||
<main>
|
<main>
|
||||||
{!sameInstance && (
|
{!sameInstance && (
|
||||||
<p>This notification is from your other account.</p>
|
<p>
|
||||||
|
<Trans>This notification is from your other account.</Trans>
|
||||||
|
</p>
|
||||||
)}
|
)}
|
||||||
<div
|
<div
|
||||||
class="notification-peek"
|
class="notification-peek"
|
||||||
|
@ -186,7 +191,10 @@ export default memo(function NotificationService() {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Link to="/notifications" class="button light" onClick={onClose}>
|
<Link to="/notifications" class="button light" onClick={onClose}>
|
||||||
<span>View all notifications</span> <Icon icon="arrow-right" />
|
<span>
|
||||||
|
<Trans>View all notifications</Trans>
|
||||||
|
</span>{' '}
|
||||||
|
<Icon icon="arrow-right" />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
|
import { msg, Plural, Select, t, Trans } from '@lingui/macro';
|
||||||
|
import { useLingui } from '@lingui/react';
|
||||||
import { Fragment } from 'preact';
|
import { Fragment } from 'preact';
|
||||||
import { memo } from 'preact/compat';
|
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 { getCurrentAccountID } from '../utils/store-utils';
|
import { getCurrentAccountID } from '../utils/store-utils';
|
||||||
import useTruncated from '../utils/useTruncated';
|
import useTruncated from '../utils/useTruncated';
|
||||||
|
|
||||||
|
@ -13,7 +14,6 @@ import FollowRequestButtons from './follow-request-buttons';
|
||||||
import Icon from './icon';
|
import Icon from './icon';
|
||||||
import Link from './link';
|
import Link from './link';
|
||||||
import NameText from './name-text';
|
import NameText from './name-text';
|
||||||
import RelativeTime from './relative-time';
|
|
||||||
import Status from './status';
|
import Status from './status';
|
||||||
|
|
||||||
const NOTIFICATION_ICONS = {
|
const NOTIFICATION_ICONS = {
|
||||||
|
@ -50,7 +50,7 @@ severed_relationships = Severed relationships
|
||||||
moderation_warning = Moderation warning
|
moderation_warning = Moderation warning
|
||||||
*/
|
*/
|
||||||
|
|
||||||
function emojiText(emoji, emoji_url) {
|
function emojiText({ account, emoji, emoji_url }) {
|
||||||
let url;
|
let url;
|
||||||
let staticUrl;
|
let staticUrl;
|
||||||
if (typeof emoji_url === 'string') {
|
if (typeof emoji_url === 'string') {
|
||||||
|
@ -59,42 +59,204 @@ function emojiText(emoji, emoji_url) {
|
||||||
url = emoji_url?.url;
|
url = emoji_url?.url;
|
||||||
staticUrl = emoji_url?.staticUrl;
|
staticUrl = emoji_url?.staticUrl;
|
||||||
}
|
}
|
||||||
return url ? (
|
const emojiObject = url ? (
|
||||||
<>
|
|
||||||
reacted to your post with{' '}
|
|
||||||
<CustomEmoji url={url} staticUrl={staticUrl} alt={emoji} />
|
<CustomEmoji url={url} staticUrl={staticUrl} alt={emoji} />
|
||||||
</>
|
|
||||||
) : (
|
) : (
|
||||||
`reacted to your post with ${emoji}.`
|
emoji
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<Trans>
|
||||||
|
{account} reacted to your post with {emojiObject}
|
||||||
|
</Trans>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const contentText = {
|
const contentText = {
|
||||||
mention: 'mentioned you in their post.',
|
status: ({ account }) => <Trans>{account} published a post.</Trans>,
|
||||||
status: 'published a post.',
|
reblog: ({
|
||||||
reblog: 'boosted your post.',
|
count,
|
||||||
'reblog+account': (count) => `boosted ${count} of your posts.`,
|
account,
|
||||||
reblog_reply: 'boosted your reply.',
|
postsCount,
|
||||||
follow: 'followed you.',
|
postType,
|
||||||
follow_request: 'requested to follow you.',
|
components: { Subject },
|
||||||
favourite: 'liked your post.',
|
}) => (
|
||||||
'favourite+account': (count) => `liked ${count} of your posts.`,
|
<Plural
|
||||||
favourite_reply: 'liked your reply.',
|
value={count}
|
||||||
poll: 'A poll you have voted in or created has ended.',
|
one={
|
||||||
'poll-self': 'A poll you have created has ended.',
|
<Plural
|
||||||
'poll-voted': 'A poll you have voted in has ended.',
|
value={postsCount}
|
||||||
update: 'A post you interacted with has been edited.',
|
one={
|
||||||
'favourite+reblog': 'boosted & liked your post.',
|
<Select
|
||||||
'favourite+reblog+account': (count) =>
|
value={postType}
|
||||||
`boosted & liked ${count} of your posts.`,
|
_reply={<Trans>{account} boosted your reply.</Trans>}
|
||||||
'favourite+reblog_reply': 'boosted & liked your reply.',
|
other={<Trans>{account} boosted your post.</Trans>}
|
||||||
'admin.sign_up': 'signed up.',
|
/>
|
||||||
'admin.report': (targetAccount) => <>reported {targetAccount}</>,
|
}
|
||||||
severed_relationships: (name) => (
|
other={
|
||||||
<>
|
<Trans>
|
||||||
Lost connections with <i>{name}</i>.
|
{account} boosted {postsCount} of your posts.
|
||||||
</>
|
</Trans>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
other={
|
||||||
|
<Select
|
||||||
|
value={postType}
|
||||||
|
_reply={
|
||||||
|
<Trans>
|
||||||
|
<Subject clickable={count > 1}>
|
||||||
|
<span title={count}>{shortenNumber(count)}</span> people
|
||||||
|
</Subject>{' '}
|
||||||
|
boosted your reply.
|
||||||
|
</Trans>
|
||||||
|
}
|
||||||
|
other={
|
||||||
|
<Trans>
|
||||||
|
<Subject clickable={count > 1}>
|
||||||
|
<span title={count}>{shortenNumber(count)}</span> people
|
||||||
|
</Subject>{' '}
|
||||||
|
boosted your post.
|
||||||
|
</Trans>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
follow: ({ account, count, components: { Subject } }) => (
|
||||||
|
<Plural
|
||||||
|
value={count}
|
||||||
|
one={<Trans>{account} followed you.</Trans>}
|
||||||
|
other={
|
||||||
|
<Trans>
|
||||||
|
<Subject clickable={count > 1}>
|
||||||
|
<span title={count}>{shortenNumber(count)}</span> people
|
||||||
|
</Subject>{' '}
|
||||||
|
followed you.
|
||||||
|
</Trans>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
follow_request: ({ account }) => (
|
||||||
|
<Trans>{account} requested to follow you.</Trans>
|
||||||
|
),
|
||||||
|
favourite: ({
|
||||||
|
account,
|
||||||
|
count,
|
||||||
|
postsCount,
|
||||||
|
postType,
|
||||||
|
components: { Subject },
|
||||||
|
}) => (
|
||||||
|
<Plural
|
||||||
|
value={count}
|
||||||
|
one={
|
||||||
|
<Plural
|
||||||
|
value={postsCount}
|
||||||
|
one={
|
||||||
|
<Select
|
||||||
|
value={postType}
|
||||||
|
_reply={<Trans>{account} liked your reply.</Trans>}
|
||||||
|
other={<Trans>{account} liked your post.</Trans>}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
other={
|
||||||
|
<Trans>
|
||||||
|
{account} liked {postsCount} of your posts.
|
||||||
|
</Trans>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
other={
|
||||||
|
<Select
|
||||||
|
value={postType}
|
||||||
|
_reply={
|
||||||
|
<Trans>
|
||||||
|
<Subject clickable={count > 1}>
|
||||||
|
<span title={count}>{shortenNumber(count)}</span> people
|
||||||
|
</Subject>{' '}
|
||||||
|
liked your reply.
|
||||||
|
</Trans>
|
||||||
|
}
|
||||||
|
other={
|
||||||
|
<Trans>
|
||||||
|
<Subject clickable={count > 1}>
|
||||||
|
<span title={count}>{shortenNumber(count)}</span> people
|
||||||
|
</Subject>{' '}
|
||||||
|
liked your post.
|
||||||
|
</Trans>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
poll: () => t`A poll you have voted in or created has ended.`,
|
||||||
|
'poll-self': () => t`A poll you have created has ended.`,
|
||||||
|
'poll-voted': () => t`A poll you have voted in has ended.`,
|
||||||
|
update: () => t`A post you interacted with has been edited.`,
|
||||||
|
'favourite+reblog': ({
|
||||||
|
count,
|
||||||
|
account,
|
||||||
|
postsCount,
|
||||||
|
postType,
|
||||||
|
components: { Subject },
|
||||||
|
}) => (
|
||||||
|
<Plural
|
||||||
|
value={count}
|
||||||
|
one={
|
||||||
|
<Plural
|
||||||
|
value={postsCount}
|
||||||
|
one={
|
||||||
|
<Select
|
||||||
|
value={postType}
|
||||||
|
_reply={<Trans>{account} boosted & liked your reply.</Trans>}
|
||||||
|
other={<Trans>{account} boosted & liked your post.</Trans>}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
other={
|
||||||
|
<Trans>
|
||||||
|
{account} boosted & liked {postsCount} of your posts.
|
||||||
|
</Trans>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
other={
|
||||||
|
<Select
|
||||||
|
value={postType}
|
||||||
|
_reply={
|
||||||
|
<Trans>
|
||||||
|
<Subject clickable={count > 1}>
|
||||||
|
<span title={count}>{shortenNumber(count)}</span> people
|
||||||
|
</Subject>{' '}
|
||||||
|
boosted & liked your reply.
|
||||||
|
</Trans>
|
||||||
|
}
|
||||||
|
other={
|
||||||
|
<Trans>
|
||||||
|
<Subject clickable={count > 1}>
|
||||||
|
<span title={count}>{shortenNumber(count)}</span> people
|
||||||
|
</Subject>{' '}
|
||||||
|
boosted & liked your post.
|
||||||
|
</Trans>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
'admin.sign_up': ({ account }) => <Trans>{account} signed up.</Trans>,
|
||||||
|
'admin.report': ({ account, targetAccount }) => (
|
||||||
|
<Trans>
|
||||||
|
{account} reported {targetAccount}
|
||||||
|
</Trans>
|
||||||
|
),
|
||||||
|
severed_relationships: ({ name }) => (
|
||||||
|
<Trans>
|
||||||
|
Lost connections with <i>{name}</i>.
|
||||||
|
</Trans>
|
||||||
|
),
|
||||||
|
moderation_warning: () => (
|
||||||
|
<b>
|
||||||
|
<Trans>Moderation warning</Trans>
|
||||||
|
</b>
|
||||||
),
|
),
|
||||||
moderation_warning: <b>Moderation warning</b>,
|
|
||||||
emoji_reaction: emojiText,
|
emoji_reaction: emojiText,
|
||||||
'pleroma:emoji_reaction': emojiText,
|
'pleroma:emoji_reaction': emojiText,
|
||||||
};
|
};
|
||||||
|
@ -102,34 +264,33 @@ const contentText = {
|
||||||
// account_suspension, domain_block, user_domain_block
|
// account_suspension, domain_block, user_domain_block
|
||||||
const SEVERED_RELATIONSHIPS_TEXT = {
|
const SEVERED_RELATIONSHIPS_TEXT = {
|
||||||
account_suspension: ({ from, targetName }) => (
|
account_suspension: ({ from, targetName }) => (
|
||||||
<>
|
<Trans>
|
||||||
An admin from <i>{from}</i> has suspended <i>{targetName}</i>, which means
|
An admin from <i>{from}</i> has suspended <i>{targetName}</i>, which means
|
||||||
you can no longer receive updates from them or interact with them.
|
you can no longer receive updates from them or interact with them.
|
||||||
</>
|
</Trans>
|
||||||
),
|
),
|
||||||
domain_block: ({ from, targetName, followersCount, followingCount }) => (
|
domain_block: ({ from, targetName, followersCount, followingCount }) => (
|
||||||
<>
|
<Trans>
|
||||||
An admin from <i>{from}</i> has blocked <i>{targetName}</i>. Affected
|
An admin from <i>{from}</i> has blocked <i>{targetName}</i>. Affected
|
||||||
followers: {followersCount}, followings: {followingCount}.
|
followers: {followersCount}, followings: {followingCount}.
|
||||||
</>
|
</Trans>
|
||||||
),
|
),
|
||||||
user_domain_block: ({ targetName, followersCount, followingCount }) => (
|
user_domain_block: ({ targetName, followersCount, followingCount }) => (
|
||||||
<>
|
<Trans>
|
||||||
You have blocked <i>{targetName}</i>. Removed followers: {followersCount},
|
You have blocked <i>{targetName}</i>. Removed followers: {followersCount},
|
||||||
followings: {followingCount}.
|
followings: {followingCount}.
|
||||||
</>
|
</Trans>
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
const MODERATION_WARNING_TEXT = {
|
const MODERATION_WARNING_TEXT = {
|
||||||
none: 'Your account has received a moderation warning.',
|
none: msg`Your account has received a moderation warning.`,
|
||||||
disable: 'Your account has been disabled.',
|
disable: msg`Your account has been disabled.`,
|
||||||
mark_statuses_as_sensitive:
|
mark_statuses_as_sensitive: msg`Some of your posts have been marked as sensitive.`,
|
||||||
'Some of your posts have been marked as sensitive.',
|
delete_statuses: msg`Some of your posts have been deleted.`,
|
||||||
delete_statuses: 'Some of your posts have been deleted.',
|
sensitive: msg`Your posts will be marked as sensitive from now on.`,
|
||||||
sensitive: 'Your posts will be marked as sensitive from now on.',
|
silence: msg`Your account has been limited.`,
|
||||||
silence: 'Your account has been limited.',
|
suspend: msg`Your account has been suspended.`,
|
||||||
suspend: 'Your account has been suspended.',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const AVATARS_LIMIT = 30;
|
const AVATARS_LIMIT = 30;
|
||||||
|
@ -140,6 +301,7 @@ function Notification({
|
||||||
isStatic,
|
isStatic,
|
||||||
disableContextMenu,
|
disableContextMenu,
|
||||||
}) {
|
}) {
|
||||||
|
const { _ } = useLingui();
|
||||||
const {
|
const {
|
||||||
id,
|
id,
|
||||||
status,
|
status,
|
||||||
|
@ -157,6 +319,11 @@ function Notification({
|
||||||
} = notification;
|
} = notification;
|
||||||
let { type } = notification;
|
let { type } = notification;
|
||||||
|
|
||||||
|
if (type === 'mention' && !status) {
|
||||||
|
// Could be deleted
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// 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
|
||||||
const actualStatus = status?.reblog || status;
|
const actualStatus = status?.reblog || status;
|
||||||
const actualStatusID = actualStatus?.id;
|
const actualStatusID = actualStatus?.id;
|
||||||
|
@ -189,37 +356,37 @@ function Notification({
|
||||||
let text;
|
let text;
|
||||||
if (type === 'poll') {
|
if (type === 'poll') {
|
||||||
text = contentText[isSelf ? 'poll-self' : isVoted ? 'poll-voted' : 'poll'];
|
text = contentText[isSelf ? 'poll-self' : isVoted ? 'poll-voted' : 'poll'];
|
||||||
} else if (
|
|
||||||
type === 'reblog' ||
|
|
||||||
type === 'favourite' ||
|
|
||||||
type === 'favourite+reblog'
|
|
||||||
) {
|
|
||||||
if (_statuses?.length > 1) {
|
|
||||||
text = contentText[`${type}+account`];
|
|
||||||
} else if (isReplyToOthers) {
|
|
||||||
text = contentText[`${type}_reply`];
|
|
||||||
} else {
|
|
||||||
text = contentText[type];
|
|
||||||
}
|
|
||||||
} else if (contentText[type]) {
|
} else if (contentText[type]) {
|
||||||
text = contentText[type];
|
text = contentText[type];
|
||||||
} else {
|
} else {
|
||||||
// Anticipate unhandled notification types, possibly from Mastodon forks or non-Mastodon instances
|
// Anticipate unhandled notification types, possibly from Mastodon forks or non-Mastodon instances
|
||||||
// This surfaces the error to the user, hoping that users will report it
|
// This surfaces the error to the user, hoping that users will report it
|
||||||
text = `[Unknown notification type: ${type}]`;
|
text = t`[Unknown notification type: ${type}]`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const Subject = ({ clickable, ...props }) =>
|
||||||
|
clickable ? (
|
||||||
|
<b tabIndex="0" onClick={handleOpenGenericAccounts} {...props} />
|
||||||
|
) : (
|
||||||
|
<b {...props} />
|
||||||
|
);
|
||||||
|
|
||||||
if (typeof text === 'function') {
|
if (typeof text === 'function') {
|
||||||
const count = _statuses?.length || _accounts?.length;
|
const count =
|
||||||
|
_accounts?.length || sampleAccounts?.length || (account ? 1 : 0);
|
||||||
|
const postsCount = _statuses?.length || 0;
|
||||||
if (type === 'admin.report') {
|
if (type === 'admin.report') {
|
||||||
const targetAccount = report?.targetAccount;
|
const targetAccount = report?.targetAccount;
|
||||||
if (targetAccount) {
|
if (targetAccount) {
|
||||||
text = text(<NameText account={targetAccount} showAvatar />);
|
text = text({
|
||||||
|
account: <NameText account={account} showAvatar />,
|
||||||
|
targetAccount: <NameText account={targetAccount} showAvatar />,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} else if (type === 'severed_relationships') {
|
} else if (type === 'severed_relationships') {
|
||||||
const targetName = event?.targetName;
|
const targetName = event?.targetName;
|
||||||
if (targetName) {
|
if (targetName) {
|
||||||
text = text(targetName);
|
text = text({ name: targetName });
|
||||||
}
|
}
|
||||||
} else if (
|
} else if (
|
||||||
(type === 'emoji_reaction' || type === 'pleroma:emoji_reaction') &&
|
(type === 'emoji_reaction' || type === 'pleroma:emoji_reaction') &&
|
||||||
|
@ -232,27 +399,28 @@ function Notification({
|
||||||
emoji?.shortcode ===
|
emoji?.shortcode ===
|
||||||
notification.emoji.replace(/^:/, '').replace(/:$/, ''),
|
notification.emoji.replace(/^:/, '').replace(/:$/, ''),
|
||||||
); // Emoji object instead of string
|
); // Emoji object instead of string
|
||||||
text = text(notification.emoji, emojiURL);
|
text = text({ emoji: notification.emoji, emojiURL });
|
||||||
} else if (count) {
|
} else {
|
||||||
text = text(count);
|
text = text({
|
||||||
|
account: account && <NameText account={account} showAvatar />,
|
||||||
|
count,
|
||||||
|
postsCount,
|
||||||
|
postType: isReplyToOthers ? 'reply' : 'post',
|
||||||
|
components: { Subject },
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === 'mention' && !status) {
|
|
||||||
// Could be deleted
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const formattedCreatedAt =
|
const formattedCreatedAt =
|
||||||
notification.createdAt && new Date(notification.createdAt).toLocaleString();
|
notification.createdAt && new Date(notification.createdAt).toLocaleString();
|
||||||
|
|
||||||
const genericAccountsHeading =
|
const genericAccountsHeading =
|
||||||
{
|
{
|
||||||
'favourite+reblog': 'Boosted/Liked by…',
|
'favourite+reblog': t`Boosted/Liked by…`,
|
||||||
favourite: 'Liked by…',
|
favourite: t`Liked by…`,
|
||||||
reblog: 'Boosted by…',
|
reblog: t`Boosted by…`,
|
||||||
follow: 'Followed by…',
|
follow: t`Followed by…`,
|
||||||
}[type] || 'Accounts';
|
}[type] || t`Accounts`;
|
||||||
const handleOpenGenericAccounts = () => {
|
const handleOpenGenericAccounts = () => {
|
||||||
states.showGenericAccounts = {
|
states.showGenericAccounts = {
|
||||||
heading: genericAccountsHeading,
|
heading: genericAccountsHeading,
|
||||||
|
@ -291,48 +459,7 @@ function Notification({
|
||||||
<div class="notification-content">
|
<div class="notification-content">
|
||||||
{type !== 'mention' && (
|
{type !== 'mention' && (
|
||||||
<>
|
<>
|
||||||
<p>
|
<p>{text}</p>
|
||||||
{!/poll|update|severed_relationships/i.test(type) && (
|
|
||||||
<>
|
|
||||||
{_accounts?.length > 1 ? (
|
|
||||||
<>
|
|
||||||
<b tabIndex="0" onClick={handleOpenGenericAccounts}>
|
|
||||||
<span title={_accounts.length}>
|
|
||||||
{shortenNumber(_accounts.length)}
|
|
||||||
</span>{' '}
|
|
||||||
people
|
|
||||||
</b>{' '}
|
|
||||||
</>
|
|
||||||
) : notificationsCount > 1 ? (
|
|
||||||
<>
|
|
||||||
<b>
|
|
||||||
<span title={notificationsCount}>
|
|
||||||
{shortenNumber(notificationsCount)}
|
|
||||||
</span>{' '}
|
|
||||||
people
|
|
||||||
</b>{' '}
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
account && (
|
|
||||||
<>
|
|
||||||
<NameText account={account} showAvatar />{' '}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{text}
|
|
||||||
{type === 'mention' && (
|
|
||||||
<span class="insignificant">
|
|
||||||
{' '}
|
|
||||||
•{' '}
|
|
||||||
<RelativeTime
|
|
||||||
datetime={notification.createdAt}
|
|
||||||
format="micro"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
{type === 'follow_request' && (
|
{type === 'follow_request' && (
|
||||||
<FollowRequestButtons accountID={account.id} />
|
<FollowRequestButtons accountID={account.id} />
|
||||||
)}
|
)}
|
||||||
|
@ -348,23 +475,26 @@ function Notification({
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
>
|
>
|
||||||
|
<Trans>
|
||||||
Learn more <Icon icon="external" size="s" />
|
Learn more <Icon icon="external" size="s" />
|
||||||
|
</Trans>
|
||||||
</a>
|
</a>
|
||||||
.
|
.
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{type === 'moderation_warning' && !!moderation_warning && (
|
{type === 'moderation_warning' && !!moderation_warning && (
|
||||||
<div>
|
<div>
|
||||||
{MODERATION_WARNING_TEXT[moderation_warning.action]}
|
{_(MODERATION_WARNING_TEXT[moderation_warning.action]())}
|
||||||
<br />
|
<br />
|
||||||
<a
|
<a
|
||||||
href={`/disputes/strikes/${moderation_warning.id}`}
|
href={`/disputes/strikes/${moderation_warning.id}`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
>
|
>
|
||||||
|
<Trans>
|
||||||
Learn more <Icon icon="external" size="s" />
|
Learn more <Icon icon="external" size="s" />
|
||||||
|
</Trans>
|
||||||
</a>
|
</a>
|
||||||
.
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
@ -541,7 +671,7 @@ function Notification({
|
||||||
|
|
||||||
function TruncatedLink(props) {
|
function TruncatedLink(props) {
|
||||||
const ref = useTruncated();
|
const ref = useTruncated();
|
||||||
return <Link {...props} data-read-more="Read more →" ref={ref} />;
|
return <Link {...props} data-read-more={t`Read more →`} ref={ref} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default memo(Notification, (oldProps, newProps) => {
|
export default memo(Notification, (oldProps, newProps) => {
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { Plural, t, Trans } from '@lingui/macro';
|
||||||
import { useState } from 'preact/hooks';
|
import { useState } from 'preact/hooks';
|
||||||
|
|
||||||
import shortenNumber from '../utils/shorten-number';
|
import shortenNumber from '../utils/shorten-number';
|
||||||
|
@ -75,11 +76,15 @@ export default function Poll({
|
||||||
<div class="poll-options">
|
<div class="poll-options">
|
||||||
{options.map((option, i) => {
|
{options.map((option, i) => {
|
||||||
const { title, votesCount: optionVotesCount } = option;
|
const { title, votesCount: optionVotesCount } = option;
|
||||||
const percentage = pollVotesCount
|
const ratio = pollVotesCount
|
||||||
? ((optionVotesCount / pollVotesCount) * 100).toFixed(
|
? optionVotesCount / pollVotesCount
|
||||||
roundPrecision,
|
: 0;
|
||||||
)
|
const percentage = ratio
|
||||||
: 0; // check if current poll choice is the leading one
|
? ratio.toLocaleString(i18n.locale || undefined, {
|
||||||
|
style: 'percent',
|
||||||
|
maximumFractionDigits: roundPrecision,
|
||||||
|
})
|
||||||
|
: '0%';
|
||||||
|
|
||||||
const isLeading =
|
const isLeading =
|
||||||
optionVotesCount > 0 &&
|
optionVotesCount > 0 &&
|
||||||
|
@ -92,7 +97,7 @@ export default function Poll({
|
||||||
isLeading ? 'poll-option-leading' : ''
|
isLeading ? 'poll-option-leading' : ''
|
||||||
}`}
|
}`}
|
||||||
style={{
|
style={{
|
||||||
'--percentage': `${percentage}%`,
|
'--percentage': `${ratio * 100}%`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class="poll-option-title">
|
<div class="poll-option-title">
|
||||||
|
@ -102,7 +107,7 @@ export default function Poll({
|
||||||
{voted && ownVotes.includes(i) && (
|
{voted && ownVotes.includes(i) && (
|
||||||
<>
|
<>
|
||||||
{' '}
|
{' '}
|
||||||
<Icon icon="check-circle" />
|
<Icon icon="check-circle" alt={t`Voted`} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -112,7 +117,7 @@ export default function Poll({
|
||||||
optionVotesCount === 1 ? '' : 's'
|
optionVotesCount === 1 ? '' : 's'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{percentage}%
|
{percentage}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -127,7 +132,7 @@ export default function Poll({
|
||||||
setShowResults(false);
|
setShowResults(false);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon icon="arrow-left" size="s" /> Hide results
|
<Icon icon="arrow-left" size="s" /> <Trans>Hide results</Trans>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
@ -176,7 +181,7 @@ export default function Poll({
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={uiState === 'loading'}
|
disabled={uiState === 'loading'}
|
||||||
>
|
>
|
||||||
Vote
|
<Trans>Vote</Trans>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</form>
|
</form>
|
||||||
|
@ -196,9 +201,9 @@ export default function Poll({
|
||||||
setUIState('default');
|
setUIState('default');
|
||||||
})();
|
})();
|
||||||
}}
|
}}
|
||||||
title="Refresh"
|
title={t`Refresh`}
|
||||||
>
|
>
|
||||||
<Icon icon="refresh" alt="Refresh" />
|
<Icon icon="refresh" alt={t`Refresh`} />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{!voted && !expired && !readOnly && optionsHaveVoteCounts && (
|
{!voted && !expired && !readOnly && optionsHaveVoteCounts && (
|
||||||
|
@ -210,30 +215,66 @@ export default function Poll({
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setShowResults(!showResults);
|
setShowResults(!showResults);
|
||||||
}}
|
}}
|
||||||
title={showResults ? 'Hide results' : 'Show results'}
|
title={showResults ? t`Hide results` : t`Show results`}
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
icon={showResults ? 'eye-open' : 'eye-close'}
|
icon={showResults ? 'eye-open' : 'eye-close'}
|
||||||
alt={showResults ? 'Hide results' : 'Show results'}
|
alt={showResults ? t`Hide results` : t`Show results`}
|
||||||
/>{' '}
|
/>{' '}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{!expired && !readOnly && ' '}
|
{!expired && !readOnly && ' '}
|
||||||
|
<Plural
|
||||||
|
value={votesCount}
|
||||||
|
one={
|
||||||
|
<Trans>
|
||||||
<span title={votesCount}>{shortenNumber(votesCount)}</span> vote
|
<span title={votesCount}>{shortenNumber(votesCount)}</span> vote
|
||||||
{votesCount === 1 ? '' : 's'}
|
</Trans>
|
||||||
|
}
|
||||||
|
other={
|
||||||
|
<Trans>
|
||||||
|
<span title={votesCount}>{shortenNumber(votesCount)}</span> votes
|
||||||
|
</Trans>
|
||||||
|
}
|
||||||
|
/>
|
||||||
{!!votersCount && votersCount !== votesCount && (
|
{!!votersCount && votersCount !== votesCount && (
|
||||||
<>
|
<>
|
||||||
{' '}
|
{' '}
|
||||||
• <span title={votersCount}>
|
•{' '}
|
||||||
{shortenNumber(votersCount)}
|
<Plural
|
||||||
</span>{' '}
|
value={votersCount}
|
||||||
|
one={
|
||||||
|
<Trans>
|
||||||
|
<span title={votersCount}>{shortenNumber(votersCount)}</span>{' '}
|
||||||
voter
|
voter
|
||||||
{votersCount === 1 ? '' : 's'}
|
</Trans>
|
||||||
|
}
|
||||||
|
other={
|
||||||
|
<Trans>
|
||||||
|
<span title={votersCount}>{shortenNumber(votersCount)}</span>{' '}
|
||||||
|
voters
|
||||||
|
</Trans>
|
||||||
|
}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
)}{' '}
|
)}{' '}
|
||||||
• {expired ? 'Ended' : 'Ending'}{' '}
|
•{' '}
|
||||||
{!!expiresAtDate && <RelativeTime datetime={expiresAtDate} />}
|
{expired ? (
|
||||||
</p>{' '}
|
!!expiresAtDate ? (
|
||||||
|
<Trans>
|
||||||
|
Ended <RelativeTime datetime={expiresAtDate} />
|
||||||
|
</Trans>
|
||||||
|
) : (
|
||||||
|
t`Ended`
|
||||||
|
)
|
||||||
|
) : !!expiresAtDate ? (
|
||||||
|
<Trans>
|
||||||
|
Ending <RelativeTime datetime={expiresAtDate} />
|
||||||
|
</Trans>
|
||||||
|
) : (
|
||||||
|
t`Ending`
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,20 +1,64 @@
|
||||||
// Twitter-style relative time component
|
import { i18n } from '@lingui/core';
|
||||||
// Seconds = 1s
|
import { t, Trans } from '@lingui/macro';
|
||||||
// Minutes = 1m
|
|
||||||
// Hours = 1h
|
|
||||||
// Days = 1d
|
|
||||||
// After 7 days, use DD/MM/YYYY or MM/DD/YYYY
|
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import dayjsTwitter from 'dayjs-twitter';
|
|
||||||
import localizedFormat from 'dayjs/plugin/localizedFormat';
|
|
||||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
|
||||||
import { useEffect, useMemo, useReducer } from 'preact/hooks';
|
import { useEffect, useMemo, useReducer } from 'preact/hooks';
|
||||||
|
|
||||||
dayjs.extend(dayjsTwitter);
|
import localeMatch from '../utils/locale-match';
|
||||||
dayjs.extend(localizedFormat);
|
import mem from '../utils/mem';
|
||||||
dayjs.extend(relativeTime);
|
|
||||||
|
|
||||||
const dtf = new Intl.DateTimeFormat();
|
const resolvedLocale = new Intl.DateTimeFormat().resolvedOptions().locale;
|
||||||
|
const DTF = mem((locale, opts = {}) => {
|
||||||
|
const lang = localeMatch([locale], [resolvedLocale]);
|
||||||
|
try {
|
||||||
|
return new Intl.DateTimeFormat(lang, opts);
|
||||||
|
} catch (e) {}
|
||||||
|
try {
|
||||||
|
return new Intl.DateTimeFormat(locale, opts);
|
||||||
|
} catch (e) {}
|
||||||
|
return new Intl.DateTimeFormat(undefined, opts);
|
||||||
|
});
|
||||||
|
const RTF = mem((locale) => new Intl.RelativeTimeFormat(locale || undefined));
|
||||||
|
|
||||||
|
const minute = 60;
|
||||||
|
const hour = 60 * minute;
|
||||||
|
const day = 24 * hour;
|
||||||
|
|
||||||
|
const rtfFromNow = (date) => {
|
||||||
|
// date = Date object
|
||||||
|
const rtf = RTF(i18n.locale);
|
||||||
|
const seconds = (date.getTime() - Date.now()) / 1000;
|
||||||
|
const absSeconds = Math.abs(seconds);
|
||||||
|
if (absSeconds < minute) {
|
||||||
|
return rtf.format(seconds, 'second');
|
||||||
|
} else if (absSeconds < hour) {
|
||||||
|
return rtf.format(Math.floor(seconds / minute), 'minute');
|
||||||
|
} else if (absSeconds < day) {
|
||||||
|
return rtf.format(Math.floor(seconds / hour), 'hour');
|
||||||
|
} else {
|
||||||
|
return rtf.format(Math.floor(seconds / day), 'day');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const twitterFromNow = (date) => {
|
||||||
|
// date = Date object
|
||||||
|
const seconds = (Date.now() - date.getTime()) / 1000;
|
||||||
|
if (seconds < minute) {
|
||||||
|
return t({
|
||||||
|
comment: 'Relative time in seconds, as short as possible',
|
||||||
|
message: `${seconds < 1 ? 1 : Math.floor(seconds)}s`,
|
||||||
|
});
|
||||||
|
} else if (seconds < hour) {
|
||||||
|
return t({
|
||||||
|
comment: 'Relative time in minutes, as short as possible',
|
||||||
|
message: `${Math.floor(seconds / minute)}m`,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return t({
|
||||||
|
comment: 'Relative time in hours, as short as possible',
|
||||||
|
message: `${Math.floor(seconds / hour)}h`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export default function RelativeTime({ datetime, format }) {
|
export default function RelativeTime({ datetime, format }) {
|
||||||
if (!datetime) return null;
|
if (!datetime) return null;
|
||||||
|
@ -27,14 +71,26 @@ export default function RelativeTime({ datetime, format }) {
|
||||||
// If date <= 1 day ago or day is within this year
|
// If date <= 1 day ago or day is within this year
|
||||||
const now = dayjs();
|
const now = dayjs();
|
||||||
const dayDiff = now.diff(date, 'day');
|
const dayDiff = now.diff(date, 'day');
|
||||||
if (dayDiff <= 1 || now.year() === date.year()) {
|
if (dayDiff <= 1) {
|
||||||
str = date.twitter();
|
str = twitterFromNow(date.toDate());
|
||||||
} else {
|
} else {
|
||||||
str = dtf.format(date.toDate());
|
const currentYear = now.year();
|
||||||
|
const dateYear = date.year();
|
||||||
|
if (dateYear === currentYear) {
|
||||||
|
str = DTF(i18n.locale, {
|
||||||
|
year: undefined,
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
}).format(date.toDate());
|
||||||
|
} else {
|
||||||
|
str = DTF(i18n.locale, {
|
||||||
|
dateStyle: 'short',
|
||||||
|
}).format(date.toDate());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!str) str = date.fromNow();
|
}
|
||||||
return [str, date.toISOString(), date.format('LLLL')];
|
if (!str) str = rtfFromNow(date.toDate());
|
||||||
|
return [str, date.toISOString(), date.toDate().toLocaleString()];
|
||||||
}, [date, format, renderCount]);
|
}, [date, format, renderCount]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import './report-modal.css';
|
import './report-modal.css';
|
||||||
|
|
||||||
|
import { msg, t, Trans } from '@lingui/macro';
|
||||||
|
import { useLingui } from '@lingui/react';
|
||||||
import { Fragment } from 'preact';
|
import { Fragment } from 'preact';
|
||||||
import { useMemo, useRef, useState } from 'preact/hooks';
|
import { useMemo, useRef, useState } from 'preact/hooks';
|
||||||
|
|
||||||
|
@ -24,26 +26,27 @@ const CATEGORIES_INFO = {
|
||||||
// description: 'Not something you want to see',
|
// description: 'Not something you want to see',
|
||||||
// },
|
// },
|
||||||
spam: {
|
spam: {
|
||||||
label: 'Spam',
|
label: msg`Spam`,
|
||||||
description: 'Malicious links, fake engagement, or repetitive replies',
|
description: msg`Malicious links, fake engagement, or repetitive replies`,
|
||||||
},
|
},
|
||||||
legal: {
|
legal: {
|
||||||
label: 'Illegal',
|
label: msg`Illegal`,
|
||||||
description: "Violates the law of your or the server's country",
|
description: msg`Violates the law of your or the server's country`,
|
||||||
},
|
},
|
||||||
violation: {
|
violation: {
|
||||||
label: 'Server rule violation',
|
label: msg`Server rule violation`,
|
||||||
description: 'Breaks specific server rules',
|
description: msg`Breaks specific server rules`,
|
||||||
stampLabel: 'Violation',
|
stampLabel: msg`Violation`,
|
||||||
},
|
},
|
||||||
other: {
|
other: {
|
||||||
label: 'Other',
|
label: msg`Other`,
|
||||||
description: "Issue doesn't fit other categories",
|
description: msg`Issue doesn't fit other categories`,
|
||||||
excludeStamp: true,
|
excludeStamp: true,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
function ReportModal({ account, post, onClose }) {
|
function ReportModal({ account, post, onClose }) {
|
||||||
|
const { _ } = useLingui();
|
||||||
const { masto } = api();
|
const { masto } = api();
|
||||||
const [uiState, setUIState] = useState('default');
|
const [uiState, setUIState] = useState('default');
|
||||||
const [username, domain] = account.acct.split('@');
|
const [username, domain] = account.acct.split('@');
|
||||||
|
@ -62,14 +65,14 @@ function ReportModal({ account, post, onClose }) {
|
||||||
return (
|
return (
|
||||||
<div class="report-modal-container">
|
<div class="report-modal-container">
|
||||||
<div class="top-controls">
|
<div class="top-controls">
|
||||||
<h1>{post ? 'Report Post' : `Report @${username}`}</h1>
|
<h1>{post ? t`Report Post` : t`Report @${username}`}</h1>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="plain4 small"
|
class="plain4 small"
|
||||||
disabled={uiState === 'loading'}
|
disabled={uiState === 'loading'}
|
||||||
onClick={() => onClose()}
|
onClick={() => onClose()}
|
||||||
>
|
>
|
||||||
<Icon icon="x" size="xl" />
|
<Icon icon="x" size="xl" alt={t`Close`} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<main>
|
<main>
|
||||||
|
@ -93,9 +96,13 @@ function ReportModal({ account, post, onClose }) {
|
||||||
key={selectedCategory}
|
key={selectedCategory}
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
>
|
>
|
||||||
{CATEGORIES_INFO[selectedCategory].stampLabel ||
|
{_(
|
||||||
CATEGORIES_INFO[selectedCategory].label}
|
CATEGORIES_INFO[selectedCategory].stampLabel ||
|
||||||
<small>Pending review</small>
|
_(CATEGORIES_INFO[selectedCategory].label),
|
||||||
|
)}
|
||||||
|
<small>
|
||||||
|
<Trans>Pending review</Trans>
|
||||||
|
</small>
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
<form
|
<form
|
||||||
|
@ -136,7 +143,7 @@ function ReportModal({ account, post, onClose }) {
|
||||||
forward,
|
forward,
|
||||||
});
|
});
|
||||||
setUIState('success');
|
setUIState('success');
|
||||||
showToast(post ? 'Post reported' : 'Profile reported');
|
showToast(post ? t`Post reported` : t`Profile reported`);
|
||||||
onClose();
|
onClose();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
@ -144,8 +151,8 @@ function ReportModal({ account, post, onClose }) {
|
||||||
showToast(
|
showToast(
|
||||||
error?.message ||
|
error?.message ||
|
||||||
(post
|
(post
|
||||||
? 'Unable to report post'
|
? t`Unable to report post`
|
||||||
: 'Unable to report profile'),
|
: t`Unable to report profile`),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
@ -153,8 +160,8 @@ function ReportModal({ account, post, onClose }) {
|
||||||
>
|
>
|
||||||
<p>
|
<p>
|
||||||
{post
|
{post
|
||||||
? `What's the issue with this post?`
|
? t`What's the issue with this post?`
|
||||||
: `What's the issue with this profile?`}
|
: t`What's the issue with this profile?`}
|
||||||
</p>
|
</p>
|
||||||
<section class="report-categories">
|
<section class="report-categories">
|
||||||
{CATEGORIES.map((category) =>
|
{CATEGORIES.map((category) =>
|
||||||
|
@ -173,9 +180,9 @@ function ReportModal({ account, post, onClose }) {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<span>
|
<span>
|
||||||
{CATEGORIES_INFO[category].label}
|
{_(CATEGORIES_INFO[category].label)}
|
||||||
<small class="ib insignificant">
|
<small class="ib insignificant">
|
||||||
{CATEGORIES_INFO[category].description}
|
{_(CATEGORIES_INFO[category].description)}
|
||||||
</small>
|
</small>
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
|
@ -222,7 +229,9 @@ function ReportModal({ account, post, onClose }) {
|
||||||
</section>
|
</section>
|
||||||
<section class="report-comment">
|
<section class="report-comment">
|
||||||
<p>
|
<p>
|
||||||
<label for="report-comment">Additional info</label>
|
<label for="report-comment">
|
||||||
|
<Trans>Additional info</Trans>
|
||||||
|
</label>
|
||||||
</p>
|
</p>
|
||||||
<textarea
|
<textarea
|
||||||
maxlength="1000"
|
maxlength="1000"
|
||||||
|
@ -243,7 +252,9 @@ function ReportModal({ account, post, onClose }) {
|
||||||
disabled={uiState === 'loading'}
|
disabled={uiState === 'loading'}
|
||||||
/>{' '}
|
/>{' '}
|
||||||
<span>
|
<span>
|
||||||
|
<Trans>
|
||||||
Forward to <i>{domain}</i>
|
Forward to <i>{domain}</i>
|
||||||
|
</Trans>
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
</p>
|
</p>
|
||||||
|
@ -251,7 +262,7 @@ function ReportModal({ account, post, onClose }) {
|
||||||
)}
|
)}
|
||||||
<footer>
|
<footer>
|
||||||
<button type="submit" disabled={uiState === 'loading'}>
|
<button type="submit" disabled={uiState === 'loading'}>
|
||||||
Send Report
|
<Trans>Send Report</Trans>
|
||||||
</button>{' '}
|
</button>{' '}
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
@ -260,15 +271,17 @@ function ReportModal({ account, post, onClose }) {
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
try {
|
try {
|
||||||
await masto.v1.accounts.$select(account.id).mute(); // Infinite duration
|
await masto.v1.accounts.$select(account.id).mute(); // Infinite duration
|
||||||
showToast(`Muted ${username}`);
|
showToast(t`Muted ${username}`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
showToast(`Unable to mute ${username}`);
|
showToast(t`Unable to mute ${username}`);
|
||||||
}
|
}
|
||||||
// onSubmit will still run
|
// onSubmit will still run
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<Trans>
|
||||||
Send Report <small class="ib">+ Mute profile</small>
|
Send Report <small class="ib">+ Mute profile</small>
|
||||||
|
</Trans>
|
||||||
</button>{' '}
|
</button>{' '}
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
@ -277,15 +290,17 @@ function ReportModal({ account, post, onClose }) {
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
try {
|
try {
|
||||||
await masto.v1.accounts.$select(account.id).block();
|
await masto.v1.accounts.$select(account.id).block();
|
||||||
showToast(`Blocked ${username}`);
|
showToast(t`Blocked ${username}`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
showToast(`Unable to block ${username}`);
|
showToast(t`Unable to block ${username}`);
|
||||||
}
|
}
|
||||||
// onSubmit will still run
|
// onSubmit will still run
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<Trans>
|
||||||
Send Report <small class="ib">+ Block profile</small>
|
Send Report <small class="ib">+ Block profile</small>
|
||||||
|
</Trans>
|
||||||
</button>
|
</button>
|
||||||
<Loader hidden={uiState !== 'loading'} />
|
<Loader hidden={uiState !== 'loading'} />
|
||||||
</footer>
|
</footer>
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { t, Trans } from '@lingui/macro';
|
||||||
import { forwardRef } from 'preact/compat';
|
import { forwardRef } from 'preact/compat';
|
||||||
import { useImperativeHandle, useRef, useState } from 'preact/hooks';
|
import { useImperativeHandle, useRef, useState } from 'preact/hooks';
|
||||||
import { useSearchParams } from 'react-router-dom';
|
import { useSearchParams } from 'react-router-dom';
|
||||||
|
@ -68,7 +69,7 @@ const SearchForm = forwardRef((props, ref) => {
|
||||||
name="q"
|
name="q"
|
||||||
type="search"
|
type="search"
|
||||||
// autofocus
|
// autofocus
|
||||||
placeholder="Search"
|
placeholder={t`Search`}
|
||||||
dir="auto"
|
dir="auto"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
autocorrect="off"
|
autocorrect="off"
|
||||||
|
@ -198,12 +199,12 @@ const SearchForm = forwardRef((props, ref) => {
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
label: (
|
label: (
|
||||||
<>
|
<Trans>
|
||||||
{query}{' '}
|
{query}{' '}
|
||||||
<small class="insignificant">
|
<small class="insignificant">
|
||||||
‒ accounts, hashtags & posts
|
‒ accounts, hashtags & posts
|
||||||
</small>
|
</small>
|
||||||
</>
|
</Trans>
|
||||||
),
|
),
|
||||||
to: `/search?q=${encodeURIComponent(query)}`,
|
to: `/search?q=${encodeURIComponent(query)}`,
|
||||||
top: !type && !/\s/.test(query),
|
top: !type && !/\s/.test(query),
|
||||||
|
@ -211,9 +212,9 @@ const SearchForm = forwardRef((props, ref) => {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: (
|
label: (
|
||||||
<>
|
<Trans>
|
||||||
Posts with <q>{query}</q>
|
Posts with <q>{query}</q>
|
||||||
</>
|
</Trans>
|
||||||
),
|
),
|
||||||
to: `/search?q=${encodeURIComponent(query)}&type=statuses`,
|
to: `/search?q=${encodeURIComponent(query)}&type=statuses`,
|
||||||
hidden: /^https?:/.test(query),
|
hidden: /^https?:/.test(query),
|
||||||
|
@ -223,9 +224,9 @@ const SearchForm = forwardRef((props, ref) => {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: (
|
label: (
|
||||||
<>
|
<Trans>
|
||||||
Posts tagged with <mark>#{query.replace(/^#/, '')}</mark>
|
Posts tagged with <mark>#{query.replace(/^#/, '')}</mark>
|
||||||
</>
|
</Trans>
|
||||||
),
|
),
|
||||||
to: `/${instance}/t/${query.replace(/^#/, '')}`,
|
to: `/${instance}/t/${query.replace(/^#/, '')}`,
|
||||||
hidden:
|
hidden:
|
||||||
|
@ -237,9 +238,9 @@ const SearchForm = forwardRef((props, ref) => {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: (
|
label: (
|
||||||
<>
|
<Trans>
|
||||||
Look up <mark>{query}</mark>
|
Look up <mark>{query}</mark>
|
||||||
</>
|
</Trans>
|
||||||
),
|
),
|
||||||
to: `/${query}`,
|
to: `/${query}`,
|
||||||
hidden: !/^https?:/.test(query),
|
hidden: !/^https?:/.test(query),
|
||||||
|
@ -248,9 +249,9 @@ const SearchForm = forwardRef((props, ref) => {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: (
|
label: (
|
||||||
<>
|
<Trans>
|
||||||
Accounts with <q>{query}</q>
|
Accounts with <q>{query}</q>
|
||||||
</>
|
</Trans>
|
||||||
),
|
),
|
||||||
to: `/search?q=${encodeURIComponent(query)}&type=accounts`,
|
to: `/search?q=${encodeURIComponent(query)}&type=accounts`,
|
||||||
icon: 'group',
|
icon: 'group',
|
||||||
|
|
|
@ -64,6 +64,10 @@
|
||||||
}
|
}
|
||||||
#shortcuts-settings-container .shortcuts-view-mode label img {
|
#shortcuts-settings-container .shortcuts-view-mode label img {
|
||||||
max-height: 64px;
|
max-height: 64px;
|
||||||
|
|
||||||
|
&:dir(rtl) {
|
||||||
|
transform: scaleX(-1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
#shortcuts-settings-container .shortcuts-view-mode label img {
|
#shortcuts-settings-container .shortcuts-view-mode label img {
|
||||||
|
@ -82,9 +86,7 @@
|
||||||
}
|
}
|
||||||
#shortcuts-settings-container .shortcuts-view-mode label input ~ * {
|
#shortcuts-settings-container .shortcuts-view-mode label input ~ * {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
transform-origin: bottom;
|
transition: opacity 0.2s ease-out;
|
||||||
transform: scale(0.975);
|
|
||||||
transition: all 0.2s ease-out;
|
|
||||||
}
|
}
|
||||||
#shortcuts-settings-container .shortcuts-view-mode label.checked {
|
#shortcuts-settings-container .shortcuts-view-mode label.checked {
|
||||||
box-shadow: inset 0 0 0 3px var(--link-color),
|
box-shadow: inset 0 0 0 3px var(--link-color),
|
||||||
|
@ -95,7 +97,6 @@
|
||||||
label
|
label
|
||||||
input:is(:hover, :active, :checked)
|
input:is(:hover, :active, :checked)
|
||||||
~ * {
|
~ * {
|
||||||
transform: scale(1);
|
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import './shortcuts-settings.css';
|
import './shortcuts-settings.css';
|
||||||
|
|
||||||
import { useAutoAnimate } from '@formkit/auto-animate/preact';
|
import { useAutoAnimate } from '@formkit/auto-animate/preact';
|
||||||
|
import { msg, Plural, t, Trans } from '@lingui/macro';
|
||||||
|
import { useLingui } from '@lingui/react';
|
||||||
import {
|
import {
|
||||||
compressToEncodedURIComponent,
|
compressToEncodedURIComponent,
|
||||||
decompressFromEncodedURIComponent,
|
decompressFromEncodedURIComponent,
|
||||||
|
@ -43,55 +45,55 @@ const TYPES = [
|
||||||
// 'account-statuses', // Need @acct search first
|
// 'account-statuses', // Need @acct search first
|
||||||
];
|
];
|
||||||
const TYPE_TEXT = {
|
const TYPE_TEXT = {
|
||||||
following: 'Home / Following',
|
following: msg`Home / Following`,
|
||||||
notifications: 'Notifications',
|
notifications: msg`Notifications`,
|
||||||
list: 'Lists',
|
list: msg`Lists`,
|
||||||
public: 'Public (Local / Federated)',
|
public: msg`Public (Local / Federated)`,
|
||||||
search: 'Search',
|
search: msg`Search`,
|
||||||
'account-statuses': 'Account',
|
'account-statuses': msg`Account`,
|
||||||
bookmarks: 'Bookmarks',
|
bookmarks: msg`Bookmarks`,
|
||||||
favourites: 'Likes',
|
favourites: msg`Likes`,
|
||||||
hashtag: 'Hashtag',
|
hashtag: msg`Hashtag`,
|
||||||
trending: 'Trending',
|
trending: msg`Trending`,
|
||||||
mentions: 'Mentions',
|
mentions: msg`Mentions`,
|
||||||
};
|
};
|
||||||
const TYPE_PARAMS = {
|
const TYPE_PARAMS = {
|
||||||
list: [
|
list: [
|
||||||
{
|
{
|
||||||
text: 'List ID',
|
text: msg`List ID`,
|
||||||
name: 'id',
|
name: 'id',
|
||||||
notRequired: true,
|
notRequired: true,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
public: [
|
public: [
|
||||||
{
|
{
|
||||||
text: 'Local only',
|
text: msg`Local only`,
|
||||||
name: 'local',
|
name: 'local',
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Instance',
|
text: msg`Instance`,
|
||||||
name: 'instance',
|
name: 'instance',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
placeholder: 'Optional, e.g. mastodon.social',
|
placeholder: msg`Optional, e.g. mastodon.social`,
|
||||||
notRequired: true,
|
notRequired: true,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
trending: [
|
trending: [
|
||||||
{
|
{
|
||||||
text: 'Instance',
|
text: msg`Instance`,
|
||||||
name: 'instance',
|
name: 'instance',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
placeholder: 'Optional, e.g. mastodon.social',
|
placeholder: msg`Optional, e.g. mastodon.social`,
|
||||||
notRequired: true,
|
notRequired: true,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
search: [
|
search: [
|
||||||
{
|
{
|
||||||
text: 'Search term',
|
text: msg`Search term`,
|
||||||
name: 'query',
|
name: 'query',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
placeholder: 'Optional, unless for multi-column mode',
|
placeholder: msg`Optional, unless for multi-column mode`,
|
||||||
notRequired: true,
|
notRequired: true,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -108,19 +110,19 @@ const TYPE_PARAMS = {
|
||||||
text: '#',
|
text: '#',
|
||||||
name: 'hashtag',
|
name: 'hashtag',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
placeholder: 'e.g. PixelArt (Max 5, space-separated)',
|
placeholder: msg`e.g. PixelArt (Max 5, space-separated)`,
|
||||||
pattern: '[^#]+',
|
pattern: '[^#]+',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Media only',
|
text: msg`Media only`,
|
||||||
name: 'media',
|
name: 'media',
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Instance',
|
text: msg`Instance`,
|
||||||
name: 'instance',
|
name: 'instance',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
placeholder: 'Optional, e.g. mastodon.social',
|
placeholder: msg`Optional, e.g. mastodon.social`,
|
||||||
notRequired: true,
|
notRequired: true,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -132,46 +134,46 @@ const fetchAccountTitle = pmem(async ({ id }) => {
|
||||||
export const SHORTCUTS_META = {
|
export const SHORTCUTS_META = {
|
||||||
following: {
|
following: {
|
||||||
id: 'home',
|
id: 'home',
|
||||||
title: (_, index) => (index === 0 ? 'Home' : 'Following'),
|
title: (_, index) => (index === 0 ? t`Home` : t`Following`),
|
||||||
path: '/',
|
path: '/',
|
||||||
icon: 'home',
|
icon: 'home',
|
||||||
},
|
},
|
||||||
mentions: {
|
mentions: {
|
||||||
id: 'mentions',
|
id: 'mentions',
|
||||||
title: 'Mentions',
|
title: msg`Mentions`,
|
||||||
path: '/mentions',
|
path: '/mentions',
|
||||||
icon: 'at',
|
icon: 'at',
|
||||||
},
|
},
|
||||||
notifications: {
|
notifications: {
|
||||||
id: 'notifications',
|
id: 'notifications',
|
||||||
title: 'Notifications',
|
title: msg`Notifications`,
|
||||||
path: '/notifications',
|
path: '/notifications',
|
||||||
icon: 'notification',
|
icon: 'notification',
|
||||||
},
|
},
|
||||||
list: {
|
list: {
|
||||||
id: ({ id }) => (id ? 'list' : 'lists'),
|
id: ({ id }) => (id ? 'list' : 'lists'),
|
||||||
title: ({ id }) => (id ? getListTitle(id) : 'Lists'),
|
title: ({ id }) => (id ? getListTitle(id) : t`Lists`),
|
||||||
path: ({ id }) => (id ? `/l/${id}` : '/l'),
|
path: ({ id }) => (id ? `/l/${id}` : '/l'),
|
||||||
icon: 'list',
|
icon: 'list',
|
||||||
excludeViewMode: ({ id }) => (!id ? ['multi-column'] : []),
|
excludeViewMode: ({ id }) => (!id ? ['multi-column'] : []),
|
||||||
},
|
},
|
||||||
public: {
|
public: {
|
||||||
id: 'public',
|
id: 'public',
|
||||||
title: ({ local }) => (local ? 'Local' : 'Federated'),
|
title: ({ local }) => (local ? t`Local` : t`Federated`),
|
||||||
subtitle: ({ instance }) => instance || api().instance,
|
subtitle: ({ instance }) => instance || api().instance,
|
||||||
path: ({ local, instance }) => `/${instance}/p${local ? '/l' : ''}`,
|
path: ({ local, instance }) => `/${instance}/p${local ? '/l' : ''}`,
|
||||||
icon: ({ local }) => (local ? 'building' : 'earth'),
|
icon: ({ local }) => (local ? 'building' : 'earth'),
|
||||||
},
|
},
|
||||||
trending: {
|
trending: {
|
||||||
id: 'trending',
|
id: 'trending',
|
||||||
title: 'Trending',
|
title: msg`Trending`,
|
||||||
subtitle: ({ instance }) => instance || api().instance,
|
subtitle: ({ instance }) => instance || api().instance,
|
||||||
path: ({ instance }) => `/${instance}/trending`,
|
path: ({ instance }) => `/${instance}/trending`,
|
||||||
icon: 'chart',
|
icon: 'chart',
|
||||||
},
|
},
|
||||||
search: {
|
search: {
|
||||||
id: 'search',
|
id: 'search',
|
||||||
title: ({ query }) => (query ? `“${query}”` : 'Search'),
|
title: ({ query }) => (query ? `“${query}”` : t`Search`),
|
||||||
path: ({ query }) =>
|
path: ({ query }) =>
|
||||||
query
|
query
|
||||||
? `/search?q=${encodeURIComponent(query)}&type=statuses`
|
? `/search?q=${encodeURIComponent(query)}&type=statuses`
|
||||||
|
@ -187,13 +189,13 @@ export const SHORTCUTS_META = {
|
||||||
},
|
},
|
||||||
bookmarks: {
|
bookmarks: {
|
||||||
id: 'bookmarks',
|
id: 'bookmarks',
|
||||||
title: 'Bookmarks',
|
title: msg`Bookmarks`,
|
||||||
path: '/b',
|
path: '/b',
|
||||||
icon: 'bookmark',
|
icon: 'bookmark',
|
||||||
},
|
},
|
||||||
favourites: {
|
favourites: {
|
||||||
id: 'favourites',
|
id: 'favourites',
|
||||||
title: 'Likes',
|
title: msg`Likes`,
|
||||||
path: '/f',
|
path: '/f',
|
||||||
icon: 'heart',
|
icon: 'heart',
|
||||||
},
|
},
|
||||||
|
@ -210,6 +212,7 @@ export const SHORTCUTS_META = {
|
||||||
};
|
};
|
||||||
|
|
||||||
function ShortcutsSettings({ onClose }) {
|
function ShortcutsSettings({ onClose }) {
|
||||||
|
const { _ } = useLingui();
|
||||||
const snapStates = useSnapshot(states);
|
const snapStates = useSnapshot(states);
|
||||||
const { shortcuts } = snapStates;
|
const { shortcuts } = snapStates;
|
||||||
const [showForm, setShowForm] = useState(false);
|
const [showForm, setShowForm] = useState(false);
|
||||||
|
@ -221,12 +224,12 @@ function ShortcutsSettings({ onClose }) {
|
||||||
<div id="shortcuts-settings-container" class="sheet" tabindex="-1">
|
<div id="shortcuts-settings-container" class="sheet" tabindex="-1">
|
||||||
{!!onClose && (
|
{!!onClose && (
|
||||||
<button type="button" class="sheet-close" onClick={onClose}>
|
<button type="button" class="sheet-close" onClick={onClose}>
|
||||||
<Icon icon="x" />
|
<Icon icon="x" alt={t`Close`} />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<header>
|
<header>
|
||||||
<h2>
|
<h2>
|
||||||
<Icon icon="shortcut" /> Shortcuts{' '}
|
<Icon icon="shortcut" /> <Trans>Shortcuts</Trans>{' '}
|
||||||
<sup
|
<sup
|
||||||
style={{
|
style={{
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
|
@ -234,27 +237,29 @@ function ShortcutsSettings({ onClose }) {
|
||||||
textTransform: 'uppercase',
|
textTransform: 'uppercase',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
beta
|
<Trans>beta</Trans>
|
||||||
</sup>
|
</sup>
|
||||||
</h2>
|
</h2>
|
||||||
</header>
|
</header>
|
||||||
<main>
|
<main>
|
||||||
<p>Specify a list of shortcuts that'll appear as:</p>
|
<p>
|
||||||
|
<Trans>Specify a list of shortcuts that'll appear as:</Trans>
|
||||||
|
</p>
|
||||||
<div class="shortcuts-view-mode">
|
<div class="shortcuts-view-mode">
|
||||||
{[
|
{[
|
||||||
{
|
{
|
||||||
value: 'float-button',
|
value: 'float-button',
|
||||||
label: 'Floating button',
|
label: t`Floating button`,
|
||||||
imgURL: floatingButtonUrl,
|
imgURL: floatingButtonUrl,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'tab-menu-bar',
|
value: 'tab-menu-bar',
|
||||||
label: 'Tab/Menu bar',
|
label: t`Tab/Menu bar`,
|
||||||
imgURL: tabMenuBarUrl,
|
imgURL: tabMenuBarUrl,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'multi-column',
|
value: 'multi-column',
|
||||||
label: 'Multi-column',
|
label: t`Multi-column`,
|
||||||
imgURL: multiColumnUrl,
|
imgURL: multiColumnUrl,
|
||||||
},
|
},
|
||||||
].map(({ value, label, imgURL }) => {
|
].map(({ value, label, imgURL }) => {
|
||||||
|
@ -291,9 +296,13 @@ function ShortcutsSettings({ onClose }) {
|
||||||
SHORTCUTS_META[type];
|
SHORTCUTS_META[type];
|
||||||
if (typeof title === 'function') {
|
if (typeof title === 'function') {
|
||||||
title = title(shortcut, i);
|
title = title(shortcut, i);
|
||||||
|
} else {
|
||||||
|
title = _(title);
|
||||||
}
|
}
|
||||||
if (typeof subtitle === 'function') {
|
if (typeof subtitle === 'function') {
|
||||||
subtitle = subtitle(shortcut, i);
|
subtitle = subtitle(shortcut, i);
|
||||||
|
} else {
|
||||||
|
subtitle = _(subtitle);
|
||||||
}
|
}
|
||||||
if (typeof icon === 'function') {
|
if (typeof icon === 'function') {
|
||||||
icon = icon(shortcut, i);
|
icon = icon(shortcut, i);
|
||||||
|
@ -317,7 +326,7 @@ function ShortcutsSettings({ onClose }) {
|
||||||
)}
|
)}
|
||||||
{excludedViewMode && (
|
{excludedViewMode && (
|
||||||
<span class="tag">
|
<span class="tag">
|
||||||
Not available in current view mode
|
<Trans>Not available in current view mode</Trans>
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
|
@ -336,7 +345,7 @@ function ShortcutsSettings({ onClose }) {
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon icon="arrow-up" alt="Move up" />
|
<Icon icon="arrow-up" alt={t`Move up`} />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
@ -352,7 +361,7 @@ function ShortcutsSettings({ onClose }) {
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon icon="arrow-down" alt="Move down" />
|
<Icon icon="arrow-down" alt={t`Move down`} />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
@ -364,7 +373,7 @@ function ShortcutsSettings({ onClose }) {
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon icon="pencil" alt="Edit" />
|
<Icon icon="pencil" alt={t`Edit`} />
|
||||||
</button>
|
</button>
|
||||||
{/* <button
|
{/* <button
|
||||||
type="button"
|
type="button"
|
||||||
|
@ -385,7 +394,9 @@ function ShortcutsSettings({ onClose }) {
|
||||||
<div class="ui-state insignificant">
|
<div class="ui-state insignificant">
|
||||||
<Icon icon="info" />{' '}
|
<Icon icon="info" />{' '}
|
||||||
<small>
|
<small>
|
||||||
|
<Trans>
|
||||||
Add more than one shortcut/column to make this work.
|
Add more than one shortcut/column to make this work.
|
||||||
|
</Trans>
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@ -394,10 +405,11 @@ function ShortcutsSettings({ onClose }) {
|
||||||
<div class="ui-state insignificant">
|
<div class="ui-state insignificant">
|
||||||
<p>
|
<p>
|
||||||
{snapStates.settings.shortcutsViewMode === 'multi-column'
|
{snapStates.settings.shortcutsViewMode === 'multi-column'
|
||||||
? 'No columns yet. Tap on the Add column button.'
|
? t`No columns yet. Tap on the Add column button.`
|
||||||
: 'No shortcuts yet. Tap on the Add shortcut button.'}
|
: t`No shortcuts yet. Tap on the Add shortcut button.`}
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
|
<Trans>
|
||||||
Not sure what to add?
|
Not sure what to add?
|
||||||
<br />
|
<br />
|
||||||
Try adding{' '}
|
Try adding{' '}
|
||||||
|
@ -418,14 +430,15 @@ function ShortcutsSettings({ onClose }) {
|
||||||
Home / Following and Notifications
|
Home / Following and Notifications
|
||||||
</a>{' '}
|
</a>{' '}
|
||||||
first.
|
first.
|
||||||
|
</Trans>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<p class="insignificant">
|
<p class="insignificant">
|
||||||
{shortcuts.length >= SHORTCUTS_LIMIT &&
|
{shortcuts.length >= SHORTCUTS_LIMIT &&
|
||||||
(snapStates.settings.shortcutsViewMode === 'multi-column'
|
(snapStates.settings.shortcutsViewMode === 'multi-column'
|
||||||
? `Max ${SHORTCUTS_LIMIT} columns`
|
? t`Max ${SHORTCUTS_LIMIT} columns`
|
||||||
: `Max ${SHORTCUTS_LIMIT} shortcuts`)}
|
: t`Max ${SHORTCUTS_LIMIT} shortcuts`)}
|
||||||
</p>
|
</p>
|
||||||
<p
|
<p
|
||||||
style={{
|
style={{
|
||||||
|
@ -439,7 +452,7 @@ function ShortcutsSettings({ onClose }) {
|
||||||
class="light"
|
class="light"
|
||||||
onClick={() => setShowImportExport(true)}
|
onClick={() => setShowImportExport(true)}
|
||||||
>
|
>
|
||||||
Import/export
|
<Trans>Import/export</Trans>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
@ -449,8 +462,8 @@ function ShortcutsSettings({ onClose }) {
|
||||||
<Icon icon="plus" />{' '}
|
<Icon icon="plus" />{' '}
|
||||||
<span>
|
<span>
|
||||||
{snapStates.settings.shortcutsViewMode === 'multi-column'
|
{snapStates.settings.shortcutsViewMode === 'multi-column'
|
||||||
? 'Add column…'
|
? t`Add column…`
|
||||||
: 'Add shortcut…'}
|
: t`Add shortcut…`}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</p>
|
</p>
|
||||||
|
@ -497,9 +510,9 @@ function ShortcutsSettings({ onClose }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const FORM_NOTES = {
|
const FORM_NOTES = {
|
||||||
list: `Specific list is optional. For multi-column mode, list is required, else the column will not be shown.`,
|
list: msg`Specific list is optional. For multi-column mode, list is required, else the column will not be shown.`,
|
||||||
search: `For multi-column mode, search term is required, else the column will not be shown.`,
|
search: msg`For multi-column mode, search term is required, else the column will not be shown.`,
|
||||||
hashtag: 'Multiple hashtags are supported. Space-separated.',
|
hashtag: msg`Multiple hashtags are supported. Space-separated.`,
|
||||||
};
|
};
|
||||||
|
|
||||||
function ShortcutForm({
|
function ShortcutForm({
|
||||||
|
@ -509,10 +522,10 @@ function ShortcutForm({
|
||||||
shortcutIndex,
|
shortcutIndex,
|
||||||
onClose,
|
onClose,
|
||||||
}) {
|
}) {
|
||||||
|
const { _ } = useLingui();
|
||||||
console.log('shortcut', shortcut);
|
console.log('shortcut', shortcut);
|
||||||
const editMode = !!shortcut;
|
const editMode = !!shortcut;
|
||||||
const [currentType, setCurrentType] = useState(shortcut?.type || null);
|
const [currentType, setCurrentType] = useState(shortcut?.type || null);
|
||||||
const { masto } = api();
|
|
||||||
|
|
||||||
const [uiState, setUIState] = useState('default');
|
const [uiState, setUIState] = useState('default');
|
||||||
const [lists, setLists] = useState([]);
|
const [lists, setLists] = useState([]);
|
||||||
|
@ -564,11 +577,11 @@ function ShortcutForm({
|
||||||
<div id="shortcut-settings-form" class="sheet">
|
<div id="shortcut-settings-form" class="sheet">
|
||||||
{!!onClose && (
|
{!!onClose && (
|
||||||
<button type="button" class="sheet-close" onClick={onClose}>
|
<button type="button" class="sheet-close" onClick={onClose}>
|
||||||
<Icon icon="x" />
|
<Icon icon="x" alt={t`Close`} />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<header>
|
<header>
|
||||||
<h2>{editMode ? 'Edit' : 'Add'} shortcut</h2>
|
<h2>{editMode ? t`Edit shortcut` : t`Add shortcut`}</h2>
|
||||||
</header>
|
</header>
|
||||||
<main tabindex="-1">
|
<main tabindex="-1">
|
||||||
<form
|
<form
|
||||||
|
@ -603,7 +616,9 @@ function ShortcutForm({
|
||||||
>
|
>
|
||||||
<p>
|
<p>
|
||||||
<label>
|
<label>
|
||||||
<span>Timeline</span>
|
<span>
|
||||||
|
<Trans>Timeline</Trans>
|
||||||
|
</span>
|
||||||
<select
|
<select
|
||||||
required
|
required
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
@ -616,7 +631,7 @@ function ShortcutForm({
|
||||||
>
|
>
|
||||||
<option></option>
|
<option></option>
|
||||||
{TYPES.map((type) => (
|
{TYPES.map((type) => (
|
||||||
<option value={type}>{TYPE_TEXT[type]}</option>
|
<option value={type}>{_(TYPE_TEXT[type])}</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
|
@ -627,7 +642,9 @@ function ShortcutForm({
|
||||||
return (
|
return (
|
||||||
<p>
|
<p>
|
||||||
<label>
|
<label>
|
||||||
<span>List</span>
|
<span>
|
||||||
|
<Trans>List</Trans>
|
||||||
|
</span>
|
||||||
<select
|
<select
|
||||||
name="id"
|
name="id"
|
||||||
required={!notRequired}
|
required={!notRequired}
|
||||||
|
@ -648,12 +665,12 @@ function ShortcutForm({
|
||||||
return (
|
return (
|
||||||
<p>
|
<p>
|
||||||
<label>
|
<label>
|
||||||
<span>{text}</span>{' '}
|
<span>{_(text)}</span>{' '}
|
||||||
<input
|
<input
|
||||||
type={type}
|
type={type}
|
||||||
switch={type === 'checkbox' || undefined}
|
switch={type === 'checkbox' || undefined}
|
||||||
name={name}
|
name={name}
|
||||||
placeholder={placeholder}
|
placeholder={_(placeholder)}
|
||||||
required={type === 'text' && !notRequired}
|
required={type === 'text' && !notRequired}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
list={
|
list={
|
||||||
|
@ -683,7 +700,7 @@ function ShortcutForm({
|
||||||
{!!FORM_NOTES[currentType] && (
|
{!!FORM_NOTES[currentType] && (
|
||||||
<p class="form-note insignificant">
|
<p class="form-note insignificant">
|
||||||
<Icon icon="info" />
|
<Icon icon="info" />
|
||||||
{FORM_NOTES[currentType]}
|
{_(FORM_NOTES[currentType])}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
<footer>
|
<footer>
|
||||||
|
@ -692,7 +709,7 @@ function ShortcutForm({
|
||||||
class="block"
|
class="block"
|
||||||
disabled={disabled || uiState === 'loading'}
|
disabled={disabled || uiState === 'loading'}
|
||||||
>
|
>
|
||||||
{editMode ? 'Save' : 'Add'}
|
{editMode ? t`Save` : t`Add`}
|
||||||
</button>
|
</button>
|
||||||
{editMode && (
|
{editMode && (
|
||||||
<button
|
<button
|
||||||
|
@ -703,7 +720,7 @@ function ShortcutForm({
|
||||||
onClose?.();
|
onClose?.();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Remove
|
<Trans>Remove</Trans>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</footer>
|
</footer>
|
||||||
|
@ -714,6 +731,7 @@ function ShortcutForm({
|
||||||
}
|
}
|
||||||
|
|
||||||
function ImportExport({ shortcuts, onClose }) {
|
function ImportExport({ shortcuts, onClose }) {
|
||||||
|
const { _ } = useLingui();
|
||||||
const { masto } = api();
|
const { masto } = api();
|
||||||
const shortcutsStr = useMemo(() => {
|
const shortcutsStr = useMemo(() => {
|
||||||
if (!shortcuts) return '';
|
if (!shortcuts) return '';
|
||||||
|
@ -759,26 +777,30 @@ function ImportExport({ shortcuts, onClose }) {
|
||||||
<div id="import-export-container" class="sheet">
|
<div id="import-export-container" class="sheet">
|
||||||
{!!onClose && (
|
{!!onClose && (
|
||||||
<button type="button" class="sheet-close" onClick={onClose}>
|
<button type="button" class="sheet-close" onClick={onClose}>
|
||||||
<Icon icon="x" />
|
<Icon icon="x" alt={t`Close`} />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<header>
|
<header>
|
||||||
<h2>
|
<h2>
|
||||||
|
<Trans>
|
||||||
Import/Export <small class="ib insignificant">Shortcuts</small>
|
Import/Export <small class="ib insignificant">Shortcuts</small>
|
||||||
|
</Trans>
|
||||||
</h2>
|
</h2>
|
||||||
</header>
|
</header>
|
||||||
<main tabindex="-1">
|
<main tabindex="-1">
|
||||||
<section>
|
<section>
|
||||||
<h3>
|
<h3>
|
||||||
<Icon icon="arrow-down-circle" size="l" class="insignificant" />{' '}
|
<Icon icon="arrow-down-circle" size="l" class="insignificant" />{' '}
|
||||||
<span>Import</span>
|
<span>
|
||||||
|
<Trans>Import</Trans>
|
||||||
|
</span>
|
||||||
</h3>
|
</h3>
|
||||||
<p class="field-button">
|
<p class="field-button">
|
||||||
<input
|
<input
|
||||||
ref={shortcutsImportFieldRef}
|
ref={shortcutsImportFieldRef}
|
||||||
type="text"
|
type="text"
|
||||||
name="import"
|
name="import"
|
||||||
placeholder="Paste shortcuts here"
|
placeholder={t`Paste shortcuts here`}
|
||||||
class="block"
|
class="block"
|
||||||
onInput={(e) => {
|
onInput={(e) => {
|
||||||
setImportShortcutStr(e.target.value);
|
setImportShortcutStr(e.target.value);
|
||||||
|
@ -794,7 +816,7 @@ function ImportExport({ shortcuts, onClose }) {
|
||||||
setImportUIState('cloud-downloading');
|
setImportUIState('cloud-downloading');
|
||||||
const currentAccount = getCurrentAccountID();
|
const currentAccount = getCurrentAccountID();
|
||||||
showToast(
|
showToast(
|
||||||
'Downloading saved shortcuts from instance server…',
|
t`Downloading saved shortcuts from instance server…`,
|
||||||
);
|
);
|
||||||
try {
|
try {
|
||||||
const relationships =
|
const relationships =
|
||||||
|
@ -823,10 +845,10 @@ function ImportExport({ shortcuts, onClose }) {
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
setImportUIState('error');
|
setImportUIState('error');
|
||||||
showToast('Unable to download shortcuts');
|
showToast(t`Unable to download shortcuts`);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
title="Download shortcuts from instance server"
|
title={t`Download shortcuts from instance server`}
|
||||||
>
|
>
|
||||||
<Icon icon="cloud" />
|
<Icon icon="cloud" />
|
||||||
<Icon icon="arrow-down" />
|
<Icon icon="arrow-down" />
|
||||||
|
@ -861,7 +883,7 @@ function ImportExport({ shortcuts, onClose }) {
|
||||||
*
|
*
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
{TYPE_TEXT[shortcut.type]}
|
{_(TYPE_TEXT[shortcut.type])}
|
||||||
{shortcut.type === 'list' && ' ⚠️'}{' '}
|
{shortcut.type === 'list' && ' ⚠️'}{' '}
|
||||||
{TYPE_PARAMS[shortcut.type]?.map?.(
|
{TYPE_PARAMS[shortcut.type]?.map?.(
|
||||||
({ text, name, type }) =>
|
({ text, name, type }) =>
|
||||||
|
@ -883,28 +905,37 @@ function ImportExport({ shortcuts, onClose }) {
|
||||||
))}
|
))}
|
||||||
</ol>
|
</ol>
|
||||||
<p>
|
<p>
|
||||||
<small>* Exists in current shortcuts</small>
|
<small>
|
||||||
|
<Trans>* Exists in current shortcuts</Trans>
|
||||||
|
</small>
|
||||||
<br />
|
<br />
|
||||||
<small>
|
<small>
|
||||||
⚠️ List may not work if it's from a different account.
|
⚠️{' '}
|
||||||
|
<Trans>
|
||||||
|
List may not work if it's from a different account.
|
||||||
|
</Trans>
|
||||||
</small>
|
</small>
|
||||||
</p>
|
</p>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{importUIState === 'error' && (
|
{importUIState === 'error' && (
|
||||||
<p class="error">
|
<p class="error">
|
||||||
<small>⚠️ Invalid settings format</small>
|
<small>
|
||||||
|
⚠️ <Trans>Invalid settings format</Trans>
|
||||||
|
</small>
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
<p>
|
<p>
|
||||||
{hasCurrentSettings && (
|
{hasCurrentSettings && (
|
||||||
<>
|
<>
|
||||||
<MenuConfirm
|
<MenuConfirm
|
||||||
confirmLabel="Append to current shortcuts?"
|
confirmLabel={t`Append to current shortcuts?`}
|
||||||
menuFooter={
|
menuFooter={
|
||||||
<div class="footer">
|
<div class="footer">
|
||||||
Only shortcuts that don’t exist in current shortcuts will
|
<Trans>
|
||||||
be appended.
|
Only shortcuts that don’t exist in current shortcuts
|
||||||
|
will be appended.
|
||||||
|
</Trans>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
@ -923,7 +954,7 @@ function ImportExport({ shortcuts, onClose }) {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
if (!nonUniqueShortcuts.length) {
|
if (!nonUniqueShortcuts.length) {
|
||||||
showToast('No new shortcuts to import');
|
showToast(t`No new shortcuts to import`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let newShortcuts = [
|
let newShortcuts = [
|
||||||
|
@ -938,8 +969,8 @@ function ImportExport({ shortcuts, onClose }) {
|
||||||
states.shortcuts = newShortcuts;
|
states.shortcuts = newShortcuts;
|
||||||
showToast(
|
showToast(
|
||||||
exceededLimit
|
exceededLimit
|
||||||
? `Shortcuts imported. Exceeded max ${SHORTCUTS_LIMIT}, so the rest are not imported.`
|
? t`Shortcuts imported. Exceeded max ${SHORTCUTS_LIMIT}, so the rest are not imported.`
|
||||||
: 'Shortcuts imported',
|
: t`Shortcuts imported`,
|
||||||
);
|
);
|
||||||
onClose?.();
|
onClose?.();
|
||||||
}}
|
}}
|
||||||
|
@ -949,7 +980,7 @@ function ImportExport({ shortcuts, onClose }) {
|
||||||
class="plain2"
|
class="plain2"
|
||||||
disabled={!parsedImportShortcutStr}
|
disabled={!parsedImportShortcutStr}
|
||||||
>
|
>
|
||||||
Import & append…
|
<Trans>Import & append…</Trans>
|
||||||
</button>
|
</button>
|
||||||
</MenuConfirm>{' '}
|
</MenuConfirm>{' '}
|
||||||
</>
|
</>
|
||||||
|
@ -957,13 +988,13 @@ function ImportExport({ shortcuts, onClose }) {
|
||||||
<MenuConfirm
|
<MenuConfirm
|
||||||
confirmLabel={
|
confirmLabel={
|
||||||
hasCurrentSettings
|
hasCurrentSettings
|
||||||
? 'Override current shortcuts?'
|
? t`Override current shortcuts?`
|
||||||
: 'Import shortcuts?'
|
: t`Import shortcuts?`
|
||||||
}
|
}
|
||||||
menuItemClassName={hasCurrentSettings ? 'danger' : undefined}
|
menuItemClassName={hasCurrentSettings ? 'danger' : undefined}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
states.shortcuts = parsedImportShortcutStr;
|
states.shortcuts = parsedImportShortcutStr;
|
||||||
showToast('Shortcuts imported');
|
showToast(t`Shortcuts imported`);
|
||||||
onClose?.();
|
onClose?.();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -972,7 +1003,7 @@ function ImportExport({ shortcuts, onClose }) {
|
||||||
class="plain2"
|
class="plain2"
|
||||||
disabled={!parsedImportShortcutStr}
|
disabled={!parsedImportShortcutStr}
|
||||||
>
|
>
|
||||||
{hasCurrentSettings ? 'or override…' : 'Import…'}
|
{hasCurrentSettings ? t`or override…` : t`Import…`}
|
||||||
</button>
|
</button>
|
||||||
</MenuConfirm>
|
</MenuConfirm>
|
||||||
</p>
|
</p>
|
||||||
|
@ -980,7 +1011,9 @@ function ImportExport({ shortcuts, onClose }) {
|
||||||
<section>
|
<section>
|
||||||
<h3>
|
<h3>
|
||||||
<Icon icon="arrow-up-circle" size="l" class="insignificant" />{' '}
|
<Icon icon="arrow-up-circle" size="l" class="insignificant" />{' '}
|
||||||
<span>Export</span>
|
<span>
|
||||||
|
<Trans>Export</Trans>
|
||||||
|
</span>
|
||||||
</h3>
|
</h3>
|
||||||
<p>
|
<p>
|
||||||
<input
|
<input
|
||||||
|
@ -994,10 +1027,10 @@ function ImportExport({ shortcuts, onClose }) {
|
||||||
// Copy url to clipboard
|
// Copy url to clipboard
|
||||||
try {
|
try {
|
||||||
navigator.clipboard.writeText(e.target.value);
|
navigator.clipboard.writeText(e.target.value);
|
||||||
showToast('Shortcuts copied');
|
showToast(t`Shortcuts copied`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
showToast('Unable to copy shortcuts');
|
showToast(t`Unable to copy shortcuts`);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
dir="auto"
|
dir="auto"
|
||||||
|
@ -1011,14 +1044,17 @@ function ImportExport({ shortcuts, onClose }) {
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
try {
|
try {
|
||||||
navigator.clipboard.writeText(shortcutsStr);
|
navigator.clipboard.writeText(shortcutsStr);
|
||||||
showToast('Shortcut settings copied');
|
showToast(t`Shortcut settings copied`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
showToast('Unable to copy shortcut settings');
|
showToast(t`Unable to copy shortcut settings`);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon icon="clipboard" /> <span>Copy</span>
|
<Icon icon="clipboard" />{' '}
|
||||||
|
<span>
|
||||||
|
<Trans>Copy</Trans>
|
||||||
|
</span>
|
||||||
</button>{' '}
|
</button>{' '}
|
||||||
{navigator?.share &&
|
{navigator?.share &&
|
||||||
navigator?.canShare?.({
|
navigator?.canShare?.({
|
||||||
|
@ -1035,11 +1071,14 @@ function ImportExport({ shortcuts, onClose }) {
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
alert("Sharing doesn't seem to work.");
|
alert(t`Sharing doesn't seem to work.`);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon icon="share" /> <span>Share</span>
|
<Icon icon="share" />{' '}
|
||||||
|
<span>
|
||||||
|
<Trans>Share</Trans>
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
)}{' '}
|
)}{' '}
|
||||||
{states.settings.shortcutSettingsCloudImportExport && (
|
{states.settings.shortcutSettingsCloudImportExport && (
|
||||||
|
@ -1077,22 +1116,22 @@ function ImportExport({ shortcuts, onClose }) {
|
||||||
} else {
|
} else {
|
||||||
newNote = `${note}\n\n\n<phanpy-shortcuts-settings>${settingsJSON}</phanpy-shortcuts-settings>`;
|
newNote = `${note}\n\n\n<phanpy-shortcuts-settings>${settingsJSON}</phanpy-shortcuts-settings>`;
|
||||||
}
|
}
|
||||||
showToast('Saving shortcuts to instance server…');
|
showToast(t`Saving shortcuts to instance server…`);
|
||||||
await masto.v1.accounts
|
await masto.v1.accounts
|
||||||
.$select(currentAccount)
|
.$select(currentAccount)
|
||||||
.note.create({
|
.note.create({
|
||||||
comment: newNote,
|
comment: newNote,
|
||||||
});
|
});
|
||||||
setImportUIState('default');
|
setImportUIState('default');
|
||||||
showToast('Shortcuts saved');
|
showToast(t`Shortcuts saved`);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
setImportUIState('error');
|
setImportUIState('error');
|
||||||
showToast('Unable to save shortcuts');
|
showToast(t`Unable to save shortcuts`);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
title="Sync to instance server"
|
title={t`Sync to instance server`}
|
||||||
>
|
>
|
||||||
<Icon icon="cloud" />
|
<Icon icon="cloud" />
|
||||||
<Icon icon="arrow-up" />
|
<Icon icon="arrow-up" />
|
||||||
|
@ -1100,14 +1139,20 @@ function ImportExport({ shortcuts, onClose }) {
|
||||||
)}{' '}
|
)}{' '}
|
||||||
{shortcutsStr.length > 0 && (
|
{shortcutsStr.length > 0 && (
|
||||||
<small class="insignificant ib">
|
<small class="insignificant ib">
|
||||||
{shortcutsStr.length} characters
|
<Plural
|
||||||
|
value={shortcutsStr.length}
|
||||||
|
one="# character"
|
||||||
|
other="# characters"
|
||||||
|
/>
|
||||||
</small>
|
</small>
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
{!!shortcutsStr && (
|
{!!shortcutsStr && (
|
||||||
<details>
|
<details>
|
||||||
<summary class="insignificant">
|
<summary class="insignificant">
|
||||||
<small>Raw Shortcuts JSON</small>
|
<small>
|
||||||
|
<Trans>Raw Shortcuts JSON</Trans>
|
||||||
|
</small>
|
||||||
</summary>
|
</summary>
|
||||||
<textarea style={{ width: '100%' }} rows={10} readOnly>
|
<textarea style={{ width: '100%' }} rows={10} readOnly>
|
||||||
{JSON.stringify(shortcuts.filter(Boolean), null, 2)}
|
{JSON.stringify(shortcuts.filter(Boolean), null, 2)}
|
||||||
|
@ -1118,8 +1163,11 @@ function ImportExport({ shortcuts, onClose }) {
|
||||||
{states.settings.shortcutSettingsCloudImportExport && (
|
{states.settings.shortcutSettingsCloudImportExport && (
|
||||||
<footer>
|
<footer>
|
||||||
<p>
|
<p>
|
||||||
<Icon icon="cloud" /> Import/export settings from/to instance
|
<Icon icon="cloud" />{' '}
|
||||||
server (Very experimental)
|
<Trans>
|
||||||
|
Import/export settings from/to instance server (Very
|
||||||
|
experimental)
|
||||||
|
</Trans>
|
||||||
</p>
|
</p>
|
||||||
</footer>
|
</footer>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import './shortcuts.css';
|
import './shortcuts.css';
|
||||||
|
|
||||||
|
import { t, Trans } from '@lingui/macro';
|
||||||
|
import { useLingui } from '@lingui/react';
|
||||||
import { MenuDivider } from '@szhsin/react-menu';
|
import { MenuDivider } from '@szhsin/react-menu';
|
||||||
import { memo } from 'preact/compat';
|
import { memo } from 'preact/compat';
|
||||||
import { useRef, useState } from 'preact/hooks';
|
import { useRef, useState } from 'preact/hooks';
|
||||||
|
@ -20,6 +22,7 @@ import Menu2 from './menu2';
|
||||||
import SubMenu2 from './submenu2';
|
import SubMenu2 from './submenu2';
|
||||||
|
|
||||||
function Shortcuts() {
|
function Shortcuts() {
|
||||||
|
const { _ } = useLingui();
|
||||||
const { instance } = api();
|
const { instance } = api();
|
||||||
const snapStates = useSnapshot(states);
|
const snapStates = useSnapshot(states);
|
||||||
const { shortcuts, settings } = snapStates;
|
const { shortcuts, settings } = snapStates;
|
||||||
|
@ -57,9 +60,13 @@ function Shortcuts() {
|
||||||
}
|
}
|
||||||
if (typeof title === 'function') {
|
if (typeof title === 'function') {
|
||||||
title = title(data, i);
|
title = title(data, i);
|
||||||
|
} else {
|
||||||
|
title = _(title);
|
||||||
}
|
}
|
||||||
if (typeof subtitle === 'function') {
|
if (typeof subtitle === 'function') {
|
||||||
subtitle = subtitle(data, i);
|
subtitle = subtitle(data, i);
|
||||||
|
} else {
|
||||||
|
subtitle = _(subtitle);
|
||||||
}
|
}
|
||||||
if (typeof icon === 'function') {
|
if (typeof icon === 'function') {
|
||||||
icon = icon(data, i);
|
icon = icon(data, i);
|
||||||
|
@ -176,7 +183,7 @@ function Shortcuts() {
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon icon="shortcut" size="xl" alt="Shortcuts" />
|
<Icon icon="shortcut" size="xl" alt={t`Shortcuts`} />
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
@ -198,7 +205,9 @@ function Shortcuts() {
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<MenuLink to="/l">
|
<MenuLink to="/l">
|
||||||
<span>All Lists</span>
|
<span>
|
||||||
|
<Trans>All Lists</Trans>
|
||||||
|
</span>
|
||||||
</MenuLink>
|
</MenuLink>
|
||||||
<MenuDivider />
|
<MenuDivider />
|
||||||
{lists?.map((list) => (
|
{lists?.map((list) => (
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,3 +1,4 @@
|
||||||
|
import { t, Trans } from '@lingui/macro';
|
||||||
import { memo } from 'preact/compat';
|
import { memo } from 'preact/compat';
|
||||||
import {
|
import {
|
||||||
useCallback,
|
useCallback,
|
||||||
|
@ -427,7 +428,7 @@ function Timeline({
|
||||||
headerStart
|
headerStart
|
||||||
) : (
|
) : (
|
||||||
<Link to="/" class="button plain home-button">
|
<Link to="/" class="button plain home-button">
|
||||||
<Icon icon="home" size="l" />
|
<Icon icon="home" size="l" alt={t`Home`} />
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -443,7 +444,7 @@ function Timeline({
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleLoadNewPosts}
|
onClick={handleLoadNewPosts}
|
||||||
>
|
>
|
||||||
<Icon icon="arrow-up" /> New posts
|
<Icon icon="arrow-up" /> <Trans>New posts</Trans>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</header>
|
</header>
|
||||||
|
@ -509,11 +510,13 @@ function Timeline({
|
||||||
onClick={() => loadItems()}
|
onClick={() => loadItems()}
|
||||||
style={{ marginBlockEnd: '6em' }}
|
style={{ marginBlockEnd: '6em' }}
|
||||||
>
|
>
|
||||||
Show more…
|
<Trans>Show more…</Trans>
|
||||||
</button>
|
</button>
|
||||||
</InView>
|
</InView>
|
||||||
) : (
|
) : (
|
||||||
<p class="ui-state insignificant">The end.</p>
|
<p class="ui-state insignificant">
|
||||||
|
<Trans>The end.</Trans>
|
||||||
|
</p>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
) : uiState === 'loading' ? (
|
) : uiState === 'loading' ? (
|
||||||
|
@ -542,7 +545,7 @@ function Timeline({
|
||||||
<br />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
<button type="button" onClick={() => loadItems(!items.length)}>
|
<button type="button" onClick={() => loadItems(!items.length)}>
|
||||||
Try again
|
<Trans>Try again</Trans>
|
||||||
</button>
|
</button>
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
@ -874,7 +877,7 @@ function StatusCarousel({ title, class: className, children }) {
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon icon="chevron-left" />
|
<Icon icon="chevron-left" alt={t`Previous`} />
|
||||||
</button>{' '}
|
</button>{' '}
|
||||||
<button
|
<button
|
||||||
ref={endButtonRef}
|
ref={endButtonRef}
|
||||||
|
@ -891,7 +894,7 @@ function StatusCarousel({ title, class: className, children }) {
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon icon="chevron-right" />
|
<Icon icon="chevron-right" alt={t`Next`} />
|
||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
</header>
|
</header>
|
||||||
|
@ -931,14 +934,14 @@ function TimelineStatusCompact({ status, instance, filterContext }) {
|
||||||
>
|
>
|
||||||
{!!snapStates.statusThreadNumber[sKey] ? (
|
{!!snapStates.statusThreadNumber[sKey] ? (
|
||||||
<div class="status-thread-badge">
|
<div class="status-thread-badge">
|
||||||
<Icon icon="thread" size="s" />
|
<Icon icon="thread" size="s" alt={t`Thread`} />
|
||||||
{snapStates.statusThreadNumber[sKey]
|
{snapStates.statusThreadNumber[sKey]
|
||||||
? ` ${snapStates.statusThreadNumber[sKey]}/X`
|
? ` ${snapStates.statusThreadNumber[sKey]}/X`
|
||||||
: ''}
|
: ''}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div class="status-thread-badge">
|
<div class="status-thread-badge">
|
||||||
<Icon icon="thread" size="s" />
|
<Icon icon="thread" size="s" alt={t`Thread`} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div
|
<div
|
||||||
|
@ -952,7 +955,15 @@ function TimelineStatusCompact({ status, instance, filterContext }) {
|
||||||
class="status-filtered-badge badge-meta horizontal"
|
class="status-filtered-badge badge-meta horizontal"
|
||||||
title={filterInfo?.titlesStr || ''}
|
title={filterInfo?.titlesStr || ''}
|
||||||
>
|
>
|
||||||
<span>Filtered</span>: <span>{filterInfo?.titlesStr || ''}</span>
|
{filterInfo?.titlesStr ? (
|
||||||
|
<Trans>
|
||||||
|
<span>Filtered</span>: <span>{filterInfo.titlesStr}</span>
|
||||||
|
</Trans>
|
||||||
|
) : (
|
||||||
|
<span>
|
||||||
|
<Trans>Filtered</Trans>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</b>
|
</b>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
@ -961,7 +972,7 @@ function TimelineStatusCompact({ status, instance, filterContext }) {
|
||||||
<>
|
<>
|
||||||
{' '}
|
{' '}
|
||||||
<span class="spoiler-badge">
|
<span class="spoiler-badge">
|
||||||
<Icon icon="eye-close" size="s" />
|
<Icon icon="eye-close" size="s" alt={t`Content warning`} />
|
||||||
</span>
|
</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import './translation-block.css';
|
import './translation-block.css';
|
||||||
|
|
||||||
|
import { t, Trans } from '@lingui/macro';
|
||||||
import pRetry from 'p-retry';
|
import pRetry from 'p-retry';
|
||||||
import pThrottle from 'p-throttle';
|
import pThrottle from 'p-throttle';
|
||||||
import { useEffect, useRef, useState } from 'preact/hooks';
|
import { useEffect, useRef, useState } from 'preact/hooks';
|
||||||
|
@ -148,7 +149,7 @@ function TranslationBlock({
|
||||||
<div class="status-translation-block-mini">
|
<div class="status-translation-block-mini">
|
||||||
<Icon
|
<Icon
|
||||||
icon="translate"
|
icon="translate"
|
||||||
alt={`Auto-translated from ${sourceLangText}`}
|
alt={t`Auto-translated from ${sourceLangText}`}
|
||||||
/>
|
/>
|
||||||
<output
|
<output
|
||||||
lang={targetLang}
|
lang={targetLang}
|
||||||
|
@ -186,12 +187,12 @@ function TranslationBlock({
|
||||||
<Icon icon="translate" />{' '}
|
<Icon icon="translate" />{' '}
|
||||||
<span>
|
<span>
|
||||||
{uiState === 'loading'
|
{uiState === 'loading'
|
||||||
? 'Translating…'
|
? t`Translating…`
|
||||||
: sourceLanguage && sourceLangText && !detectedLang
|
: sourceLanguage && sourceLangText && !detectedLang
|
||||||
? autoDetected
|
? autoDetected
|
||||||
? `Translate from ${sourceLangText} (auto-detected)`
|
? t`Translate from ${sourceLangText} (auto-detected)`
|
||||||
: `Translate from ${sourceLangText}`
|
: t`Translate from ${sourceLangText}`
|
||||||
: `Translate`}
|
: t`Translate`}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</summary>
|
</summary>
|
||||||
|
@ -207,7 +208,15 @@ function TranslationBlock({
|
||||||
>
|
>
|
||||||
{sourceLanguages.map((l) => (
|
{sourceLanguages.map((l) => (
|
||||||
<option value={l.code}>
|
<option value={l.code}>
|
||||||
{l.code === 'auto' ? `Auto (${detectedLang ?? '…'})` : l.name}
|
{l.code === 'auto'
|
||||||
|
? t`Auto (${detectedLang ?? '…'})`
|
||||||
|
: `${localeCode2Text({
|
||||||
|
code: l.code,
|
||||||
|
fallback: l.name,
|
||||||
|
})} (${localeCode2Text({
|
||||||
|
code: l.code,
|
||||||
|
locale: l.code,
|
||||||
|
})})`}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>{' '}
|
</select>{' '}
|
||||||
|
@ -215,7 +224,9 @@ function TranslationBlock({
|
||||||
<Loader abrupt hidden={uiState !== 'loading'} />
|
<Loader abrupt hidden={uiState !== 'loading'} />
|
||||||
</div>
|
</div>
|
||||||
{uiState === 'error' ? (
|
{uiState === 'error' ? (
|
||||||
<p class="ui-state">Failed to translate</p>
|
<p class="ui-state">
|
||||||
|
<Trans>Failed to translate</Trans>
|
||||||
|
</p>
|
||||||
) : (
|
) : (
|
||||||
!!translatedContent && (
|
!!translatedContent && (
|
||||||
<>
|
<>
|
||||||
|
|
|
@ -2,13 +2,19 @@ import './index.css';
|
||||||
import './app.css';
|
import './app.css';
|
||||||
import './polyfills';
|
import './polyfills';
|
||||||
|
|
||||||
|
import { i18n } from '@lingui/core';
|
||||||
|
import { t, Trans } from '@lingui/macro';
|
||||||
|
import { I18nProvider } from '@lingui/react';
|
||||||
import { render } from 'preact';
|
import { render } from 'preact';
|
||||||
import { useEffect, useState } from 'preact/hooks';
|
import { useEffect, useState } from 'preact/hooks';
|
||||||
|
|
||||||
import ComposeSuspense from './components/compose-suspense';
|
import ComposeSuspense from './components/compose-suspense';
|
||||||
|
import { initActivateLang } from './utils/lang';
|
||||||
import { initStates } from './utils/states';
|
import { initStates } from './utils/states';
|
||||||
import useTitle from './utils/useTitle';
|
import useTitle from './utils/useTitle';
|
||||||
|
|
||||||
|
initActivateLang();
|
||||||
|
|
||||||
if (window.opener) {
|
if (window.opener) {
|
||||||
console = window.opener.console;
|
console = window.opener.console;
|
||||||
}
|
}
|
||||||
|
@ -20,12 +26,12 @@ function App() {
|
||||||
|
|
||||||
useTitle(
|
useTitle(
|
||||||
editStatus
|
editStatus
|
||||||
? 'Editing source status'
|
? t`Editing source status`
|
||||||
: replyToStatus
|
: replyToStatus
|
||||||
? `Replying to @${
|
? t`Replying to @${
|
||||||
replyToStatus.account?.acct || replyToStatus.account?.username
|
replyToStatus.account?.acct || replyToStatus.account?.username
|
||||||
}`
|
}`
|
||||||
: 'Compose',
|
: t`Compose`,
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -45,14 +51,16 @@ function App() {
|
||||||
if (uiState === 'closed') {
|
if (uiState === 'closed') {
|
||||||
return (
|
return (
|
||||||
<div class="box">
|
<div class="box">
|
||||||
<p>You may close this page now.</p>
|
<p>
|
||||||
|
<Trans>You may close this page now.</Trans>
|
||||||
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
window.close();
|
window.close();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Close window
|
<Trans>Close window</Trans>
|
||||||
</button>
|
</button>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -82,4 +90,9 @@ function App() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
render(<App />, document.getElementById('app-standalone'));
|
render(
|
||||||
|
<I18nProvider i18n={i18n}>
|
||||||
|
<App />
|
||||||
|
</I18nProvider>,
|
||||||
|
document.getElementById('app-standalone'),
|
||||||
|
);
|
||||||
|
|
3633
src/locales/en.po
Normal file
3633
src/locales/en.po
Normal file
File diff suppressed because it is too large
Load diff
3633
src/locales/pseudo-LOCALE.po
Normal file
3633
src/locales/pseudo-LOCALE.po
Normal file
File diff suppressed because it is too large
Load diff
|
@ -2,6 +2,8 @@ import './index.css';
|
||||||
import './cloak-mode.css';
|
import './cloak-mode.css';
|
||||||
import './polyfills';
|
import './polyfills';
|
||||||
|
|
||||||
|
import { i18n } from '@lingui/core';
|
||||||
|
import { I18nProvider } from '@lingui/react';
|
||||||
// Polyfill needed for Firefox < 122
|
// Polyfill needed for Firefox < 122
|
||||||
// https://bugzilla.mozilla.org/show_bug.cgi?id=1423593
|
// https://bugzilla.mozilla.org/show_bug.cgi?id=1423593
|
||||||
// import '@formatjs/intl-segmenter/polyfill';
|
// import '@formatjs/intl-segmenter/polyfill';
|
||||||
|
@ -9,15 +11,20 @@ import { render } from 'preact';
|
||||||
import { HashRouter } from 'react-router-dom';
|
import { HashRouter } from 'react-router-dom';
|
||||||
|
|
||||||
import { App } from './app';
|
import { App } from './app';
|
||||||
|
import { initActivateLang } from './utils/lang';
|
||||||
|
|
||||||
|
initActivateLang();
|
||||||
|
|
||||||
if (import.meta.env.DEV) {
|
if (import.meta.env.DEV) {
|
||||||
import('preact/debug');
|
import('preact/debug');
|
||||||
}
|
}
|
||||||
|
|
||||||
render(
|
render(
|
||||||
|
<I18nProvider i18n={i18n}>
|
||||||
<HashRouter>
|
<HashRouter>
|
||||||
<App />
|
<App />
|
||||||
</HashRouter>,
|
</HashRouter>
|
||||||
|
</I18nProvider>,
|
||||||
document.getElementById('app'),
|
document.getElementById('app'),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
// NOTE: UNUSED
|
||||||
|
|
||||||
import Link from '../components/link';
|
import Link from '../components/link';
|
||||||
|
|
||||||
export default function NotFound() {
|
export default function NotFound() {
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { t, Trans } from '@lingui/macro';
|
||||||
|
import { useLingui } from '@lingui/react';
|
||||||
import { MenuItem } from '@szhsin/react-menu';
|
import { MenuItem } from '@szhsin/react-menu';
|
||||||
import {
|
import {
|
||||||
useCallback,
|
useCallback,
|
||||||
|
@ -227,33 +229,32 @@ function AccountStatuses() {
|
||||||
}
|
}
|
||||||
|
|
||||||
const [featuredTags, setFeaturedTags] = useState([]);
|
const [featuredTags, setFeaturedTags] = useState([]);
|
||||||
useTitle(
|
const { i18n } = useLingui();
|
||||||
account?.acct
|
let title = t`Account posts`;
|
||||||
? `${
|
if (account?.acct) {
|
||||||
account?.displayName
|
const acctDisplay = /@/.test(account.acct) ? '' : '@' + account.acct;
|
||||||
? `${account.displayName} (${/@/.test(account.acct) ? '' : '@'}${
|
const accountDisplay = account?.displayName
|
||||||
account.acct
|
? `${account.displayName} (${acctDisplay})`
|
||||||
})`
|
: `${acctDisplay}`;
|
||||||
: `${/@/.test(account.acct) ? '' : '@'}${account.acct}`
|
if (!excludeReplies) {
|
||||||
}${
|
title = t`${accountDisplay} (+ Replies)`;
|
||||||
!excludeReplies
|
} else if (excludeBoosts) {
|
||||||
? ' (+ Replies)'
|
title = t`${accountDisplay} (- Boosts)`;
|
||||||
: excludeBoosts
|
} else if (tagged) {
|
||||||
? ' (- Boosts)'
|
title = t`${accountDisplay} (#${tagged})`;
|
||||||
: tagged
|
} else if (media) {
|
||||||
? ` (#${tagged})`
|
title = t`${accountDisplay} (Media)`;
|
||||||
: media
|
} else if (month) {
|
||||||
? ' (Media)'
|
const monthYear = new Date(month).toLocaleString(i18n.locale, {
|
||||||
: month
|
|
||||||
? ` (${new Date(month).toLocaleString('default', {
|
|
||||||
month: 'long',
|
month: 'long',
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
})})`
|
});
|
||||||
: ''
|
title = t`${accountDisplay} (${monthYear})`;
|
||||||
}`
|
} else {
|
||||||
: 'Account posts',
|
title = accountDisplay;
|
||||||
'/:instance?/a/:id',
|
}
|
||||||
);
|
}
|
||||||
|
useTitle(title, '/:instance?/a/:id');
|
||||||
|
|
||||||
const fetchAccountPromiseRef = useRef();
|
const fetchAccountPromiseRef = useRef();
|
||||||
const fetchAccount = useCallback(() => {
|
const fetchAccount = useCallback(() => {
|
||||||
|
@ -317,46 +318,51 @@ function AccountStatuses() {
|
||||||
<Link
|
<Link
|
||||||
to={`/${instance}/a/${id}`}
|
to={`/${instance}/a/${id}`}
|
||||||
class="insignificant filter-clear"
|
class="insignificant filter-clear"
|
||||||
title="Clear filters"
|
title={t`Clear filters`}
|
||||||
key="clear-filters"
|
key="clear-filters"
|
||||||
>
|
>
|
||||||
<Icon icon="x" size="l" />
|
<Icon icon="x" size="l" alt={t`Clear`} />
|
||||||
</Link>
|
</Link>
|
||||||
) : (
|
) : (
|
||||||
<Icon icon="filter" class="insignificant" size="l" />
|
<Icon
|
||||||
|
icon="filter"
|
||||||
|
class="insignificant"
|
||||||
|
size="l"
|
||||||
|
alt={t`Filters`}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
<Link
|
<Link
|
||||||
to={`/${instance}/a/${id}${excludeReplies ? '?replies=1' : ''}`}
|
to={`/${instance}/a/${id}${excludeReplies ? '?replies=1' : ''}`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (excludeReplies) {
|
if (excludeReplies) {
|
||||||
showToast('Showing post with replies');
|
showToast(t`Showing post with replies`);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
class={excludeReplies ? '' : 'is-active'}
|
class={excludeReplies ? '' : 'is-active'}
|
||||||
>
|
>
|
||||||
+ Replies
|
<Trans>+ Replies</Trans>
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
to={`/${instance}/a/${id}${excludeBoosts ? '' : '?boosts=0'}`}
|
to={`/${instance}/a/${id}${excludeBoosts ? '' : '?boosts=0'}`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!excludeBoosts) {
|
if (!excludeBoosts) {
|
||||||
showToast('Showing posts without boosts');
|
showToast(t`Showing posts without boosts`);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
class={!excludeBoosts ? '' : 'is-active'}
|
class={!excludeBoosts ? '' : 'is-active'}
|
||||||
>
|
>
|
||||||
- Boosts
|
<Trans>- Boosts</Trans>
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
to={`/${instance}/a/${id}${media ? '' : '?media=1'}`}
|
to={`/${instance}/a/${id}${media ? '' : '?media=1'}`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!media) {
|
if (!media) {
|
||||||
showToast('Showing posts with media');
|
showToast(t`Showing posts with media`);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
class={media ? 'is-active' : ''}
|
class={media ? 'is-active' : ''}
|
||||||
>
|
>
|
||||||
Media
|
<Trans>Media</Trans>
|
||||||
</Link>
|
</Link>
|
||||||
{featuredTags.map((tag) => (
|
{featuredTags.map((tag) => (
|
||||||
<Link
|
<Link
|
||||||
|
@ -368,7 +374,7 @@ function AccountStatuses() {
|
||||||
}`}
|
}`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (tagged !== tag.name) {
|
if (tagged !== tag.name) {
|
||||||
showToast(`Showing posts tagged with #${tag.name}`);
|
showToast(t`Showing posts tagged with #${tag.name}`);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
class={tagged === tag.name ? 'is-active' : ''}
|
class={tagged === tag.name ? 'is-active' : ''}
|
||||||
|
@ -407,7 +413,7 @@ function AccountStatuses() {
|
||||||
const monthIndex = parseInt(month, 10) - 1;
|
const monthIndex = parseInt(month, 10) - 1;
|
||||||
const date = new Date(year, monthIndex);
|
const date = new Date(year, monthIndex);
|
||||||
showToast(
|
showToast(
|
||||||
`Showing posts in ${date.toLocaleString('default', {
|
t`Showing posts in ${date.toLocaleString(i18n.locale, {
|
||||||
month: 'long',
|
month: 'long',
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
})}`,
|
})}`,
|
||||||
|
@ -475,7 +481,7 @@ function AccountStatuses() {
|
||||||
return (
|
return (
|
||||||
<Timeline
|
<Timeline
|
||||||
key={id}
|
key={id}
|
||||||
title={`${account?.acct ? '@' + account.acct : 'Posts'}`}
|
title={`${account?.acct ? '@' + account.acct : t`Posts`}`}
|
||||||
titleComponent={
|
titleComponent={
|
||||||
<h1
|
<h1
|
||||||
class="header-double-lines header-account"
|
class="header-double-lines header-account"
|
||||||
|
@ -496,8 +502,8 @@ function AccountStatuses() {
|
||||||
}
|
}
|
||||||
id="account-statuses"
|
id="account-statuses"
|
||||||
instance={instance}
|
instance={instance}
|
||||||
emptyText="Nothing to see here yet."
|
emptyText={t`Nothing to see here yet.`}
|
||||||
errorText="Unable to load posts"
|
errorText={t`Unable to load posts`}
|
||||||
fetchItems={fetchAccountStatuses}
|
fetchItems={fetchAccountStatuses}
|
||||||
useItemID
|
useItemID
|
||||||
view={media || mediaFirst ? 'media' : undefined}
|
view={media || mediaFirst ? 'media' : undefined}
|
||||||
|
@ -519,7 +525,7 @@ function AccountStatuses() {
|
||||||
position="anchor"
|
position="anchor"
|
||||||
menuButton={
|
menuButton={
|
||||||
<button type="button" class="plain">
|
<button type="button" class="plain">
|
||||||
<Icon icon="more" size="l" />
|
<Icon icon="more" size="l" alt={t`More`} />
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
@ -538,13 +544,14 @@ function AccountStatuses() {
|
||||||
location.hash = `/${accountInstance}/a/${id}`;
|
location.hash = `/${accountInstance}/a/${id}`;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
alert('Unable to fetch account info');
|
alert(t`Unable to fetch account info`);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon icon="transfer" />{' '}
|
<Icon icon="transfer" />{' '}
|
||||||
<small class="menu-double-lines">
|
<small class="menu-double-lines">
|
||||||
|
<Trans>
|
||||||
Switch to account's instance{' '}
|
Switch to account's instance{' '}
|
||||||
{accountInstance ? (
|
{accountInstance ? (
|
||||||
<>
|
<>
|
||||||
|
@ -552,6 +559,7 @@ function AccountStatuses() {
|
||||||
(<b>{punycode.toUnicode(accountInstance)}</b>)
|
(<b>{punycode.toUnicode(accountInstance)}</b>)
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
|
</Trans>
|
||||||
</small>
|
</small>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
{!sameCurrentInstance && (
|
{!sameCurrentInstance && (
|
||||||
|
@ -566,14 +574,16 @@ function AccountStatuses() {
|
||||||
location.hash = `/${currentInstance}/a/${id}`;
|
location.hash = `/${currentInstance}/a/${id}`;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
alert('Unable to fetch account info');
|
alert(t`Unable to fetch account info`);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon icon="transfer" />{' '}
|
<Icon icon="transfer" />{' '}
|
||||||
<small class="menu-double-lines">
|
<small class="menu-double-lines">
|
||||||
|
<Trans>
|
||||||
Switch to my instance (<b>{currentInstance}</b>)
|
Switch to my instance (<b>{currentInstance}</b>)
|
||||||
|
</Trans>
|
||||||
</small>
|
</small>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
)}
|
)}
|
||||||
|
@ -584,6 +594,7 @@ function AccountStatuses() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function MonthPicker(props) {
|
function MonthPicker(props) {
|
||||||
|
const { i18n } = useLingui();
|
||||||
const {
|
const {
|
||||||
class: className,
|
class: className,
|
||||||
disabled,
|
disabled,
|
||||||
|
@ -631,7 +642,9 @@ function MonthPicker(props) {
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<option value="">Month</option>
|
<option value="">
|
||||||
|
<Trans>Month</Trans>
|
||||||
|
</option>
|
||||||
<option disabled>-----</option>
|
<option disabled>-----</option>
|
||||||
{Array.from({ length: 12 }, (_, i) => (
|
{Array.from({ length: 12 }, (_, i) => (
|
||||||
<option
|
<option
|
||||||
|
@ -641,7 +654,7 @@ function MonthPicker(props) {
|
||||||
}
|
}
|
||||||
key={i}
|
key={i}
|
||||||
>
|
>
|
||||||
{new Date(0, i).toLocaleString('default', {
|
{new Date(0, i).toLocaleString(i18n.locale, {
|
||||||
month: 'long',
|
month: 'long',
|
||||||
})}
|
})}
|
||||||
</option>
|
</option>
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import './accounts.css';
|
import './accounts.css';
|
||||||
|
|
||||||
import { useAutoAnimate } from '@formkit/auto-animate/preact';
|
import { useAutoAnimate } from '@formkit/auto-animate/preact';
|
||||||
|
import { t, Trans } from '@lingui/macro';
|
||||||
import { Menu, MenuDivider, MenuItem } from '@szhsin/react-menu';
|
import { Menu, MenuDivider, MenuItem } from '@szhsin/react-menu';
|
||||||
import { useReducer } from 'preact/hooks';
|
import { useReducer } from 'preact/hooks';
|
||||||
|
|
||||||
|
@ -29,11 +30,13 @@ function Accounts({ onClose }) {
|
||||||
<div id="accounts-container" class="sheet" tabIndex="-1">
|
<div id="accounts-container" class="sheet" tabIndex="-1">
|
||||||
{!!onClose && (
|
{!!onClose && (
|
||||||
<button type="button" class="sheet-close" onClick={onClose}>
|
<button type="button" class="sheet-close" onClick={onClose}>
|
||||||
<Icon icon="x" />
|
<Icon icon="x" alt={t`Close`} />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<header class="header-grid">
|
<header class="header-grid">
|
||||||
<h2>Accounts</h2>
|
<h2>
|
||||||
|
<Trans>Accounts</Trans>
|
||||||
|
</h2>
|
||||||
</header>
|
</header>
|
||||||
<main>
|
<main>
|
||||||
<section>
|
<section>
|
||||||
|
@ -46,7 +49,7 @@ function Accounts({ onClose }) {
|
||||||
<div>
|
<div>
|
||||||
{moreThanOneAccount && (
|
{moreThanOneAccount && (
|
||||||
<span class={`current ${isCurrent ? 'is-current' : ''}`}>
|
<span class={`current ${isCurrent ? 'is-current' : ''}`}>
|
||||||
<Icon icon="check-circle" alt="Current" />
|
<Icon icon="check-circle" alt={t`Current`} />
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
<Avatar
|
<Avatar
|
||||||
|
@ -91,18 +94,16 @@ function Accounts({ onClose }) {
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
{isDefault && moreThanOneAccount && (
|
{isDefault && moreThanOneAccount && (
|
||||||
<>
|
<>
|
||||||
<span class="tag">Default</span>{' '}
|
<span class="tag">
|
||||||
|
<Trans>Default</Trans>
|
||||||
|
</span>{' '}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<Menu2
|
<Menu2
|
||||||
align="end"
|
align="end"
|
||||||
menuButton={
|
menuButton={
|
||||||
<button
|
<button type="button" class="plain more-button">
|
||||||
type="button"
|
<Icon icon="more" size="l" alt={t`More`} />
|
||||||
title="More"
|
|
||||||
class="plain more-button"
|
|
||||||
>
|
|
||||||
<Icon icon="more" size="l" alt="More" />
|
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
@ -112,7 +113,9 @@ function Accounts({ onClose }) {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon icon="user" />
|
<Icon icon="user" />
|
||||||
<span>View profile…</span>
|
<span>
|
||||||
|
<Trans>View profile…</Trans>
|
||||||
|
</span>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuDivider />
|
<MenuDivider />
|
||||||
{moreThanOneAccount && (
|
{moreThanOneAccount && (
|
||||||
|
@ -127,7 +130,9 @@ function Accounts({ onClose }) {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon icon="check-circle" />
|
<Icon icon="check-circle" />
|
||||||
<span>Set as default</span>
|
<span>
|
||||||
|
<Trans>Set as default</Trans>
|
||||||
|
</span>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
)}
|
)}
|
||||||
<MenuConfirm
|
<MenuConfirm
|
||||||
|
@ -135,7 +140,9 @@ function Accounts({ onClose }) {
|
||||||
confirmLabel={
|
confirmLabel={
|
||||||
<>
|
<>
|
||||||
<Icon icon="exit" />
|
<Icon icon="exit" />
|
||||||
<span>Log out @{account.info.acct}?</span>
|
<span>
|
||||||
|
<Trans>Log out @{account.info.acct}?</Trans>
|
||||||
|
</span>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
disabled={!isCurrent}
|
disabled={!isCurrent}
|
||||||
|
@ -150,7 +157,9 @@ function Accounts({ onClose }) {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon icon="exit" />
|
<Icon icon="exit" />
|
||||||
<span>Log out…</span>
|
<span>
|
||||||
|
<Trans>Log out…</Trans>
|
||||||
|
</span>
|
||||||
</MenuConfirm>
|
</MenuConfirm>
|
||||||
</Menu2>
|
</Menu2>
|
||||||
</div>
|
</div>
|
||||||
|
@ -160,14 +169,19 @@ function Accounts({ onClose }) {
|
||||||
</ul>
|
</ul>
|
||||||
<p>
|
<p>
|
||||||
<Link to="/login" class="button plain2" onClick={onClose}>
|
<Link to="/login" class="button plain2" onClick={onClose}>
|
||||||
<Icon icon="plus" /> <span>Add an existing account</span>
|
<Icon icon="plus" />{' '}
|
||||||
|
<span>
|
||||||
|
<Trans>Add an existing account</Trans>
|
||||||
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
</p>
|
</p>
|
||||||
{moreThanOneAccount && (
|
{moreThanOneAccount && (
|
||||||
<p>
|
<p>
|
||||||
<small>
|
<small>
|
||||||
Note: <i>Default</i> account will always be used for first load.
|
<Trans>
|
||||||
Switched accounts will persist during the session.
|
Note: <i>Default</i> account will always be used for first
|
||||||
|
load. Switched accounts will persist during the session.
|
||||||
|
</Trans>
|
||||||
</small>
|
</small>
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { t, Trans } from '@lingui/macro';
|
||||||
import { useRef } from 'preact/hooks';
|
import { useRef } from 'preact/hooks';
|
||||||
|
|
||||||
import Timeline from '../components/timeline';
|
import Timeline from '../components/timeline';
|
||||||
|
@ -7,7 +8,7 @@ import useTitle from '../utils/useTitle';
|
||||||
const LIMIT = 20;
|
const LIMIT = 20;
|
||||||
|
|
||||||
function Bookmarks() {
|
function Bookmarks() {
|
||||||
useTitle('Bookmarks', '/b');
|
useTitle(t`Bookmarks`, '/bookmarks');
|
||||||
const { masto, instance } = api();
|
const { masto, instance } = api();
|
||||||
const bookmarksIterator = useRef();
|
const bookmarksIterator = useRef();
|
||||||
async function fetchBookmarks(firstLoad) {
|
async function fetchBookmarks(firstLoad) {
|
||||||
|
@ -19,10 +20,10 @@ function Bookmarks() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Timeline
|
<Timeline
|
||||||
title="Bookmarks"
|
title={t`Bookmarks`}
|
||||||
id="bookmarks"
|
id="bookmarks"
|
||||||
emptyText="No bookmarks yet. Go bookmark something!"
|
emptyText={`No bookmarks yet. Go bookmark something!`}
|
||||||
errorText="Unable to load bookmarks"
|
errorText={t`Unable to load bookmarks.`}
|
||||||
instance={instance}
|
instance={instance}
|
||||||
fetchItems={fetchBookmarks}
|
fetchItems={fetchBookmarks}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -2,6 +2,8 @@ import '../components/links-bar.css';
|
||||||
import './catchup.css';
|
import './catchup.css';
|
||||||
|
|
||||||
import autoAnimate from '@formkit/auto-animate';
|
import autoAnimate from '@formkit/auto-animate';
|
||||||
|
import { msg, Plural, select, t, Trans } from '@lingui/macro';
|
||||||
|
import { useLingui } from '@lingui/react';
|
||||||
import { getBlurHashAverageColor } from 'fast-blurhash';
|
import { getBlurHashAverageColor } from 'fast-blurhash';
|
||||||
import { Fragment } from 'preact';
|
import { Fragment } from 'preact';
|
||||||
import { memo } from 'preact/compat';
|
import { memo } from 'preact/compat';
|
||||||
|
@ -34,6 +36,7 @@ import db from '../utils/db';
|
||||||
import emojifyText from '../utils/emojify-text';
|
import emojifyText from '../utils/emojify-text';
|
||||||
import { isFiltered } from '../utils/filters';
|
import { isFiltered } from '../utils/filters';
|
||||||
import htmlContentLength from '../utils/html-content-length';
|
import htmlContentLength from '../utils/html-content-length';
|
||||||
|
import mem from '../utils/mem';
|
||||||
import niceDateTime from '../utils/nice-date-time';
|
import niceDateTime from '../utils/nice-date-time';
|
||||||
import shortenNumber from '../utils/shorten-number';
|
import shortenNumber from '../utils/shorten-number';
|
||||||
import showToast from '../utils/show-toast';
|
import showToast from '../utils/show-toast';
|
||||||
|
@ -48,29 +51,29 @@ import useTitle from '../utils/useTitle';
|
||||||
const FILTER_CONTEXT = 'home';
|
const FILTER_CONTEXT = 'home';
|
||||||
|
|
||||||
const RANGES = [
|
const RANGES = [
|
||||||
{ label: 'last 1 hour', value: 1 },
|
{ label: msg`last 1 hour`, value: 1 },
|
||||||
{ label: 'last 2 hours', value: 2 },
|
{ label: msg`last 2 hours`, value: 2 },
|
||||||
{ label: 'last 3 hours', value: 3 },
|
{ label: msg`last 3 hours`, value: 3 },
|
||||||
{ label: 'last 4 hours', value: 4 },
|
{ label: msg`last 4 hours`, value: 4 },
|
||||||
{ label: 'last 5 hours', value: 5 },
|
{ label: msg`last 5 hours`, value: 5 },
|
||||||
{ label: 'last 6 hours', value: 6 },
|
{ label: msg`last 6 hours`, value: 6 },
|
||||||
{ label: 'last 7 hours', value: 7 },
|
{ label: msg`last 7 hours`, value: 7 },
|
||||||
{ label: 'last 8 hours', value: 8 },
|
{ label: msg`last 8 hours`, value: 8 },
|
||||||
{ label: 'last 9 hours', value: 9 },
|
{ label: msg`last 9 hours`, value: 9 },
|
||||||
{ label: 'last 10 hours', value: 10 },
|
{ label: msg`last 10 hours`, value: 10 },
|
||||||
{ label: 'last 11 hours', value: 11 },
|
{ label: msg`last 11 hours`, value: 11 },
|
||||||
{ label: 'last 12 hours', value: 12 },
|
{ label: msg`last 12 hours`, value: 12 },
|
||||||
{ label: 'beyond 12 hours', value: 13 },
|
{ label: msg`beyond 12 hours`, value: 13 },
|
||||||
];
|
];
|
||||||
|
|
||||||
const FILTER_LABELS = [
|
const FILTER_KEYS = {
|
||||||
'Original',
|
original: msg`Original`,
|
||||||
'Replies',
|
replies: msg`Replies`,
|
||||||
'Boosts',
|
boosts: msg`Boosts`,
|
||||||
'Followed tags',
|
followedTags: msg`Followed tags`,
|
||||||
'Groups',
|
groups: msg`Groups`,
|
||||||
'Filtered',
|
filtered: msg`Filtered`,
|
||||||
];
|
};
|
||||||
const FILTER_SORTS = [
|
const FILTER_SORTS = [
|
||||||
'createdAt',
|
'createdAt',
|
||||||
'repliesCount',
|
'repliesCount',
|
||||||
|
@ -79,33 +82,23 @@ const FILTER_SORTS = [
|
||||||
'density',
|
'density',
|
||||||
];
|
];
|
||||||
const FILTER_GROUPS = [null, 'account'];
|
const FILTER_GROUPS = [null, 'account'];
|
||||||
const FILTER_VALUES = {
|
|
||||||
Filtered: 'filtered',
|
const DTF = mem(
|
||||||
Groups: 'group',
|
(locale) =>
|
||||||
Boosts: 'boost',
|
new Intl.DateTimeFormat(locale || undefined, {
|
||||||
Replies: 'reply',
|
year: 'numeric',
|
||||||
'Followed tags': 'followedTags',
|
month: 'short',
|
||||||
Original: 'original',
|
day: 'numeric',
|
||||||
};
|
hour: 'numeric',
|
||||||
const FILTER_CATEGORY_TEXT = {
|
minute: 'numeric',
|
||||||
Filtered: 'filtered posts',
|
}),
|
||||||
Groups: 'group posts',
|
);
|
||||||
Boosts: 'boosts',
|
|
||||||
Replies: 'replies',
|
|
||||||
'Followed tags': 'followed-tag posts',
|
|
||||||
Original: 'original posts',
|
|
||||||
};
|
|
||||||
const SORT_BY_TEXT = {
|
|
||||||
// asc, desc
|
|
||||||
createdAt: ['oldest', 'latest'],
|
|
||||||
repliesCount: ['fewest replies', 'most replies'],
|
|
||||||
favouritesCount: ['fewest likes', 'most likes'],
|
|
||||||
reblogsCount: ['fewest boosts', 'most boosts'],
|
|
||||||
density: ['least dense', 'most dense'],
|
|
||||||
};
|
|
||||||
|
|
||||||
function Catchup() {
|
function Catchup() {
|
||||||
useTitle('Catch-up', '/catchup');
|
const { i18n, _ } = useLingui();
|
||||||
|
const dtf = DTF(i18n.locale);
|
||||||
|
|
||||||
|
useTitle(`Catch-up`, '/catchup');
|
||||||
const { masto, instance } = api();
|
const { masto, instance } = api();
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
const id = searchParams.get('id');
|
const id = searchParams.get('id');
|
||||||
|
@ -307,23 +300,23 @@ function Catchup() {
|
||||||
}, [uiState === 'start']);
|
}, [uiState === 'start']);
|
||||||
|
|
||||||
const [filterCounts, links] = useMemo(() => {
|
const [filterCounts, links] = useMemo(() => {
|
||||||
let filtereds = 0,
|
let filtered = 0,
|
||||||
groups = 0,
|
groups = 0,
|
||||||
boosts = 0,
|
boosts = 0,
|
||||||
replies = 0,
|
replies = 0,
|
||||||
followedTags = 0,
|
followedTags = 0,
|
||||||
originals = 0;
|
original = 0;
|
||||||
const links = {};
|
const links = {};
|
||||||
for (const post of posts) {
|
for (const post of posts) {
|
||||||
if (post._filtered) {
|
if (post._filtered) {
|
||||||
filtereds++;
|
filtered++;
|
||||||
post.__FILTER = 'filtered';
|
post.__FILTER = 'filtered';
|
||||||
} else if (post.group) {
|
} else if (post.group) {
|
||||||
groups++;
|
groups++;
|
||||||
post.__FILTER = 'group';
|
post.__FILTER = 'groups';
|
||||||
} else if (post.reblog) {
|
} else if (post.reblog) {
|
||||||
boosts++;
|
boosts++;
|
||||||
post.__FILTER = 'boost';
|
post.__FILTER = 'boosts';
|
||||||
} else if (post._followedTags?.length) {
|
} else if (post._followedTags?.length) {
|
||||||
followedTags++;
|
followedTags++;
|
||||||
post.__FILTER = 'followedTags';
|
post.__FILTER = 'followedTags';
|
||||||
|
@ -332,9 +325,9 @@ function Catchup() {
|
||||||
post.inReplyToAccountId !== post.account?.id
|
post.inReplyToAccountId !== post.account?.id
|
||||||
) {
|
) {
|
||||||
replies++;
|
replies++;
|
||||||
post.__FILTER = 'reply';
|
post.__FILTER = 'replies';
|
||||||
} else {
|
} else {
|
||||||
originals++;
|
original++;
|
||||||
post.__FILTER = 'original';
|
post.__FILTER = 'original';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -401,18 +394,18 @@ function Catchup() {
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
Filtered: filtereds,
|
filtered,
|
||||||
Groups: groups,
|
groups,
|
||||||
Boosts: boosts,
|
boosts,
|
||||||
Replies: replies,
|
replies,
|
||||||
'Followed tags': followedTags,
|
followedTags,
|
||||||
Original: originals,
|
original,
|
||||||
},
|
},
|
||||||
topLinks,
|
topLinks,
|
||||||
];
|
];
|
||||||
}, [posts]);
|
}, [posts]);
|
||||||
|
|
||||||
const [selectedFilterCategory, setSelectedFilterCategory] = useState('All');
|
const [selectedFilterCategory, setSelectedFilterCategory] = useState('all');
|
||||||
const [selectedAuthor, setSelectedAuthor] = useState(null);
|
const [selectedAuthor, setSelectedAuthor] = useState(null);
|
||||||
|
|
||||||
const [range, setRange] = useState(1);
|
const [range, setRange] = useState(1);
|
||||||
|
@ -427,8 +420,8 @@ function Catchup() {
|
||||||
|
|
||||||
let filteredPosts = posts.filter((post) => {
|
let filteredPosts = posts.filter((post) => {
|
||||||
const postFilterMatches =
|
const postFilterMatches =
|
||||||
selectedFilterCategory === 'All' ||
|
selectedFilterCategory === 'all' ||
|
||||||
post.__FILTER === FILTER_VALUES[selectedFilterCategory];
|
post.__FILTER === selectedFilterCategory;
|
||||||
|
|
||||||
if (postFilterMatches) {
|
if (postFilterMatches) {
|
||||||
authorsHash[post.account.id] = post.account;
|
authorsHash[post.account.id] = post.account;
|
||||||
|
@ -599,15 +592,37 @@ function Catchup() {
|
||||||
};
|
};
|
||||||
let toast = showToast({
|
let toast = showToast({
|
||||||
duration: 5_000, // 5 seconds
|
duration: 5_000, // 5 seconds
|
||||||
text: `Showing ${
|
// Note: I'm sorry, translators
|
||||||
FILTER_CATEGORY_TEXT[selectedFilterCategory] || 'all posts'
|
text: t`Showing ${select(selectedFilterCategory, {
|
||||||
}${authorUsername ? ` by @${authorUsername}` : ''}, ${
|
all: 'all posts',
|
||||||
SORT_BY_TEXT[sortBy][sortOrderIndex]
|
original: 'original posts',
|
||||||
} first${
|
replies: 'replies',
|
||||||
!!groupBy
|
boosts: 'boosts',
|
||||||
? `, grouped by ${groupBy === 'account' ? groupByText[groupBy] : ''}`
|
followedTags: 'followed tags',
|
||||||
: ''
|
groups: 'groups',
|
||||||
}`,
|
filtered: 'filtered posts',
|
||||||
|
})}, ${select(sortBy, {
|
||||||
|
createdAt: select(sortOrder, {
|
||||||
|
asc: 'oldest',
|
||||||
|
desc: 'latest',
|
||||||
|
}),
|
||||||
|
reblogsCount: select(sortOrder, {
|
||||||
|
asc: 'fewest boosts',
|
||||||
|
desc: 'most boosts',
|
||||||
|
}),
|
||||||
|
favouritesCount: select(sortOrder, {
|
||||||
|
asc: 'fewest likes',
|
||||||
|
desc: 'most likes',
|
||||||
|
}),
|
||||||
|
repliesCount: select(sortOrder, {
|
||||||
|
asc: 'fewest replies',
|
||||||
|
desc: 'most replies',
|
||||||
|
}),
|
||||||
|
density: select(sortOrder, { asc: 'least dense', desc: 'most dense' }),
|
||||||
|
})} first${select(groupBy, {
|
||||||
|
account: ', grouped by authors',
|
||||||
|
other: '',
|
||||||
|
})}`,
|
||||||
});
|
});
|
||||||
return () => {
|
return () => {
|
||||||
toast?.hideToast?.();
|
toast?.hideToast?.();
|
||||||
|
@ -837,20 +852,20 @@ function Catchup() {
|
||||||
<NavMenu />
|
<NavMenu />
|
||||||
{uiState === 'results' && (
|
{uiState === 'results' && (
|
||||||
<Link to="/catchup" class="button plain">
|
<Link to="/catchup" class="button plain">
|
||||||
<Icon icon="history2" size="l" />
|
<Icon icon="history2" size="l" alt={t`Catch-up`} />
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
{uiState === 'start' && (
|
{uiState === 'start' && (
|
||||||
<Link to="/" class="button plain">
|
<Link to="/" class="button plain">
|
||||||
<Icon icon="home" size="l" />
|
<Icon icon="home" size="l" alt={t`Home`} />
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<h1>
|
<h1>
|
||||||
{uiState !== 'start' && (
|
{uiState !== 'start' && (
|
||||||
<>
|
<Trans>
|
||||||
Catch-up <sup>beta</sup>
|
Catch-up <sup>beta</sup>
|
||||||
</>
|
</Trans>
|
||||||
)}
|
)}
|
||||||
</h1>
|
</h1>
|
||||||
<div class="header-side">
|
<div class="header-side">
|
||||||
|
@ -862,7 +877,7 @@ function Catchup() {
|
||||||
setShowHelp(true);
|
setShowHelp(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Help
|
<Trans>Help</Trans>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -872,20 +887,27 @@ function Catchup() {
|
||||||
{uiState === 'start' && (
|
{uiState === 'start' && (
|
||||||
<div class="catchup-start">
|
<div class="catchup-start">
|
||||||
<h1>
|
<h1>
|
||||||
|
<Trans>
|
||||||
Catch-up <sup>beta</sup>
|
Catch-up <sup>beta</sup>
|
||||||
|
</Trans>
|
||||||
</h1>
|
</h1>
|
||||||
<details>
|
<details>
|
||||||
<summary>What is this?</summary>
|
<summary>
|
||||||
|
<Trans>What is this?</Trans>
|
||||||
|
</summary>
|
||||||
<p>
|
<p>
|
||||||
Catch-up is a separate timeline for your followings, offering
|
<Trans>
|
||||||
a high-level view at a glance, with a simple, email-inspired
|
Catch-up is a separate timeline for your followings,
|
||||||
interface to effortlessly sort and filter through posts.
|
offering a high-level view at a glance, with a simple,
|
||||||
|
email-inspired interface to effortlessly sort and filter
|
||||||
|
through posts.
|
||||||
|
</Trans>
|
||||||
</p>
|
</p>
|
||||||
<img
|
<img
|
||||||
src={catchupUrl}
|
src={catchupUrl}
|
||||||
width="1200"
|
width="1200"
|
||||||
height="900"
|
height="900"
|
||||||
alt="Preview of Catch-up UI"
|
alt={t`Preview of Catch-up UI`}
|
||||||
/>
|
/>
|
||||||
<p>
|
<p>
|
||||||
<button
|
<button
|
||||||
|
@ -894,13 +916,17 @@ function Catchup() {
|
||||||
e.target.closest('details').open = false;
|
e.target.closest('details').open = false;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Let's catch up
|
<Trans>Let's catch up</Trans>
|
||||||
</button>
|
</button>
|
||||||
</p>
|
</p>
|
||||||
</details>
|
</details>
|
||||||
<p>Let's catch up on the posts from your followings.</p>
|
|
||||||
<p>
|
<p>
|
||||||
<b>Show me all posts from…</b>
|
<Trans>Let's catch up on the posts from your followings.</Trans>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<b>
|
||||||
|
<Trans>Show me all posts from…</Trans>
|
||||||
|
</b>
|
||||||
</p>
|
</p>
|
||||||
<div class="catchup-form">
|
<div class="catchup-form">
|
||||||
<input
|
<input
|
||||||
|
@ -918,11 +944,11 @@ function Catchup() {
|
||||||
width: '8em',
|
width: '8em',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{RANGES[range - 1].label}
|
{_(RANGES[range - 1].label)}
|
||||||
<br />
|
<br />
|
||||||
<small class="insignificant">
|
<small class="insignificant">
|
||||||
{range == RANGES[RANGES.length - 1].value
|
{range == RANGES[RANGES.length - 1].value
|
||||||
? 'until the max'
|
? t`until the max`
|
||||||
: niceDateTime(
|
: niceDateTime(
|
||||||
new Date(Date.now() - range * 60 * 60 * 1000),
|
new Date(Date.now() - range * 60 * 60 * 1000),
|
||||||
)}
|
)}
|
||||||
|
@ -930,7 +956,7 @@ function Catchup() {
|
||||||
</span>
|
</span>
|
||||||
<datalist id="catchup-ranges">
|
<datalist id="catchup-ranges">
|
||||||
{RANGES.map(({ label, value }) => (
|
{RANGES.map(({ label, value }) => (
|
||||||
<option value={value} label={label} />
|
<option value={value} label={_(label)} />
|
||||||
))}
|
))}
|
||||||
</datalist>{' '}
|
</datalist>{' '}
|
||||||
<button
|
<button
|
||||||
|
@ -952,12 +978,13 @@ function Catchup() {
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Catch up
|
<Trans>Catch up</Trans>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{lastCatchupRange && range > lastCatchupRange ? (
|
{lastCatchupRange && range > lastCatchupRange ? (
|
||||||
<p class="catchup-info">
|
<p class="catchup-info">
|
||||||
<Icon icon="info" /> Overlaps with your last catch-up
|
<Icon icon="info" />{' '}
|
||||||
|
<Trans>Overlaps with your last catch-up</Trans>
|
||||||
</p>
|
</p>
|
||||||
) : range === RANGES[RANGES.length - 1].value &&
|
) : range === RANGES[RANGES.length - 1].value &&
|
||||||
lastCatchupEndAt ? (
|
lastCatchupEndAt ? (
|
||||||
|
@ -969,21 +996,27 @@ function Catchup() {
|
||||||
checked
|
checked
|
||||||
ref={catchupLastRef}
|
ref={catchupLastRef}
|
||||||
/>{' '}
|
/>{' '}
|
||||||
|
<Trans>
|
||||||
Until the last catch-up (
|
Until the last catch-up (
|
||||||
{dtf.format(new Date(lastCatchupEndAt))})
|
{dtf.format(new Date(lastCatchupEndAt))})
|
||||||
|
</Trans>
|
||||||
</label>
|
</label>
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
<p class="insignificant">
|
<p class="insignificant">
|
||||||
<small>
|
<small>
|
||||||
Note: your instance might only show a maximum of 800 posts in
|
<Trans>
|
||||||
the Home timeline regardless of the time range. Could be less
|
Note: your instance might only show a maximum of 800 posts
|
||||||
or more.
|
in the Home timeline regardless of the time range. Could be
|
||||||
|
less or more.
|
||||||
|
</Trans>
|
||||||
</small>
|
</small>
|
||||||
</p>
|
</p>
|
||||||
{!!prevCatchups?.length && (
|
{!!prevCatchups?.length && (
|
||||||
<div class="catchup-prev">
|
<div class="catchup-prev">
|
||||||
<p>Previously…</p>
|
<p>
|
||||||
|
<Trans>Previously…</Trans>
|
||||||
|
</p>
|
||||||
<ul>
|
<ul>
|
||||||
{prevCatchups.map((pc) => (
|
{prevCatchups.map((pc) => (
|
||||||
<li key={pc.id}>
|
<li key={pc.id}>
|
||||||
|
@ -1000,23 +1033,29 @@ function Catchup() {
|
||||||
</Link>{' '}
|
</Link>{' '}
|
||||||
<span>
|
<span>
|
||||||
<small class="ib insignificant">
|
<small class="ib insignificant">
|
||||||
{pc.count} posts
|
<Plural
|
||||||
|
value={pc.count}
|
||||||
|
one="# post"
|
||||||
|
other="# posts"
|
||||||
|
/>
|
||||||
</small>{' '}
|
</small>{' '}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="light danger small"
|
class="light danger small"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
const yes = confirm('Remove this catch-up?');
|
const yes = confirm(t`Remove this catch-up?`);
|
||||||
if (yes) {
|
if (yes) {
|
||||||
let t = showToast(`Removing Catch-up ${pc.id}`);
|
let t = showToast(
|
||||||
|
t`Removing Catch-up ${pc.id}`,
|
||||||
|
);
|
||||||
await db.catchup.del(pc.id);
|
await db.catchup.del(pc.id);
|
||||||
t?.hideToast?.();
|
t?.hideToast?.();
|
||||||
showToast(`Catch-up ${pc.id} removed`);
|
showToast(t`Catch-up ${pc.id} removed`);
|
||||||
reloadCatchups();
|
reloadCatchups();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon icon="x" />
|
<Icon icon="x" alt={t`Remove`} />
|
||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
|
@ -1025,8 +1064,10 @@ function Catchup() {
|
||||||
{prevCatchups.length >= 3 && (
|
{prevCatchups.length >= 3 && (
|
||||||
<p>
|
<p>
|
||||||
<small>
|
<small>
|
||||||
|
<Trans>
|
||||||
Note: Only max 3 will be stored. The rest will be
|
Note: Only max 3 will be stored. The rest will be
|
||||||
automatically removed.
|
automatically removed.
|
||||||
|
</Trans>
|
||||||
</small>
|
</small>
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
@ -1037,8 +1078,12 @@ function Catchup() {
|
||||||
{uiState === 'loading' && (
|
{uiState === 'loading' && (
|
||||||
<div class="ui-state catchup-start">
|
<div class="ui-state catchup-start">
|
||||||
<Loader abrupt />
|
<Loader abrupt />
|
||||||
<p class="insignificant">Fetching posts…</p>
|
<p class="insignificant">
|
||||||
<p class="insignificant">This might take a while.</p>
|
<Trans>Fetching posts…</Trans>
|
||||||
|
</p>
|
||||||
|
<p class="insignificant">
|
||||||
|
<Trans>This might take a while.</Trans>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{uiState === 'results' && (
|
{uiState === 'results' && (
|
||||||
|
@ -1057,7 +1102,7 @@ function Catchup() {
|
||||||
<aside>
|
<aside>
|
||||||
<button
|
<button
|
||||||
hidden={
|
hidden={
|
||||||
selectedFilterCategory === 'All' &&
|
selectedFilterCategory === 'all' &&
|
||||||
!selectedAuthor &&
|
!selectedAuthor &&
|
||||||
sortBy === 'createdAt' &&
|
sortBy === 'createdAt' &&
|
||||||
sortOrder === 'asc'
|
sortOrder === 'asc'
|
||||||
|
@ -1065,14 +1110,14 @@ function Catchup() {
|
||||||
type="button"
|
type="button"
|
||||||
class="plain4 small"
|
class="plain4 small"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelectedFilterCategory('All');
|
setSelectedFilterCategory('all');
|
||||||
setSelectedAuthor(null);
|
setSelectedAuthor(null);
|
||||||
setSortBy('createdAt');
|
setSortBy('createdAt');
|
||||||
setGroupBy(null);
|
setGroupBy(null);
|
||||||
setSortOrder('asc');
|
setSortOrder('asc');
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Reset filters
|
<Trans>Reset filters</Trans>
|
||||||
</button>
|
</button>
|
||||||
{links?.length > 0 && (
|
{links?.length > 0 && (
|
||||||
<button
|
<button
|
||||||
|
@ -1080,7 +1125,7 @@ function Catchup() {
|
||||||
class="plain small"
|
class="plain small"
|
||||||
onClick={() => setShowTopLinks(!showTopLinks)}
|
onClick={() => setShowTopLinks(!showTopLinks)}
|
||||||
>
|
>
|
||||||
Top links{' '}
|
<Trans>Top links</Trans>{' '}
|
||||||
<Icon
|
<Icon
|
||||||
icon="chevron-down"
|
icon="chevron-down"
|
||||||
style={{
|
style={{
|
||||||
|
@ -1196,6 +1241,7 @@ function Catchup() {
|
||||||
whiteSpace: 'nowrap',
|
whiteSpace: 'nowrap',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<Trans>
|
||||||
Shared by{' '}
|
Shared by{' '}
|
||||||
{sharers.map((s) => {
|
{sharers.map((s) => {
|
||||||
const { avatarStatic, displayName } = s;
|
const { avatarStatic, displayName } = s;
|
||||||
|
@ -1207,6 +1253,7 @@ function Catchup() {
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
</Trans>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
|
@ -1230,22 +1277,21 @@ function Catchup() {
|
||||||
name="filter-cat"
|
name="filter-cat"
|
||||||
checked={selectedFilterCategory.toLowerCase() === 'all'}
|
checked={selectedFilterCategory.toLowerCase() === 'all'}
|
||||||
onChange={() => {
|
onChange={() => {
|
||||||
setSelectedFilterCategory('All');
|
setSelectedFilterCategory('all');
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
All <span class="count">{posts.length}</span>
|
<Trans>All</Trans> <span class="count">{posts.length}</span>
|
||||||
</label>
|
</label>
|
||||||
{FILTER_LABELS.map(
|
{Object.entries(FILTER_KEYS).map(
|
||||||
(label) =>
|
([key, label]) =>
|
||||||
!!filterCounts[label] && (
|
!!filterCounts[key] && (
|
||||||
<label
|
<label
|
||||||
class="filter-cat"
|
class="filter-cat"
|
||||||
key={label}
|
key={_(label)}
|
||||||
title={
|
title={
|
||||||
(
|
((filterCounts[key] / posts.length) * 100).toFixed(
|
||||||
(filterCounts[label] / posts.length) *
|
2,
|
||||||
100
|
) + '%'
|
||||||
).toFixed(2) + '%'
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
|
@ -1253,11 +1299,11 @@ function Catchup() {
|
||||||
name="filter-cat"
|
name="filter-cat"
|
||||||
checked={
|
checked={
|
||||||
selectedFilterCategory.toLowerCase() ===
|
selectedFilterCategory.toLowerCase() ===
|
||||||
label.toLowerCase()
|
key.toLowerCase()
|
||||||
}
|
}
|
||||||
onChange={() => {
|
onChange={() => {
|
||||||
setSelectedFilterCategory(label);
|
setSelectedFilterCategory(key);
|
||||||
if (label === 'Boosts') {
|
if (key === 'boosts') {
|
||||||
setSortBy('reblogsCount');
|
setSortBy('reblogsCount');
|
||||||
setSortOrder('desc');
|
setSortOrder('desc');
|
||||||
setGroupBy(null);
|
setGroupBy(null);
|
||||||
|
@ -1265,8 +1311,8 @@ function Catchup() {
|
||||||
// setSelectedAuthor(null);
|
// setSelectedAuthor(null);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{label}{' '}
|
{_(label)}{' '}
|
||||||
<span class="count">{filterCounts[label]}</span>
|
<span class="count">{filterCounts[key]}</span>
|
||||||
</label>
|
</label>
|
||||||
),
|
),
|
||||||
)}
|
)}
|
||||||
|
@ -1319,14 +1365,20 @@ function Catchup() {
|
||||||
opacity: 0.33,
|
opacity: 0.33,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{authorCountsList.length} authors
|
<Plural
|
||||||
|
value={authorCountsList.length}
|
||||||
|
one="# author"
|
||||||
|
other="# authors"
|
||||||
|
/>
|
||||||
</small>
|
</small>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{posts.length >= 2 && (
|
{posts.length >= 2 && (
|
||||||
<div class="catchup-filters">
|
<div class="catchup-filters">
|
||||||
<span class="filter-label">Sort</span>{' '}
|
<span class="filter-label">
|
||||||
|
<Trans>Sort</Trans>
|
||||||
|
</span>{' '}
|
||||||
<fieldset class="radio-field-group">
|
<fieldset class="radio-field-group">
|
||||||
{FILTER_SORTS.map((key) => (
|
{FILTER_SORTS.map((key) => (
|
||||||
<label
|
<label
|
||||||
|
@ -1356,11 +1408,11 @@ function Catchup() {
|
||||||
/>
|
/>
|
||||||
{
|
{
|
||||||
{
|
{
|
||||||
createdAt: 'Date',
|
createdAt: t`Date`,
|
||||||
repliesCount: 'Replies',
|
repliesCount: t`Replies`,
|
||||||
favouritesCount: 'Likes',
|
favouritesCount: t`Likes`,
|
||||||
reblogsCount: 'Boosts',
|
reblogsCount: t`Boosts`,
|
||||||
density: 'Density',
|
density: t`Density`,
|
||||||
}[key]
|
}[key]
|
||||||
}
|
}
|
||||||
{sortBy === key && (sortOrder === 'asc' ? ' ↑' : ' ↓')}
|
{sortBy === key && (sortOrder === 'asc' ? ' ↑' : ' ↓')}
|
||||||
|
@ -1382,7 +1434,9 @@ function Catchup() {
|
||||||
</label>
|
</label>
|
||||||
))}
|
))}
|
||||||
</fieldset> */}
|
</fieldset> */}
|
||||||
<span class="filter-label">Group</span>{' '}
|
<span class="filter-label">
|
||||||
|
<Trans>Group</Trans>
|
||||||
|
</span>{' '}
|
||||||
<fieldset class="radio-field-group">
|
<fieldset class="radio-field-group">
|
||||||
{FILTER_GROUPS.map((key) => (
|
{FILTER_GROUPS.map((key) => (
|
||||||
<label class="filter-group" key={key || 'none'}>
|
<label class="filter-group" key={key || 'none'}>
|
||||||
|
@ -1396,8 +1450,8 @@ function Catchup() {
|
||||||
disabled={key === 'account' && selectedAuthor}
|
disabled={key === 'account' && selectedAuthor}
|
||||||
/>
|
/>
|
||||||
{{
|
{{
|
||||||
account: 'Authors',
|
account: t`Authors`,
|
||||||
}[key] || 'None'}
|
}[key] || t`None`}
|
||||||
</label>
|
</label>
|
||||||
))}
|
))}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
@ -1413,7 +1467,7 @@ function Catchup() {
|
||||||
whiteSpace: 'nowrap',
|
whiteSpace: 'nowrap',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Show all authors
|
<Trans>Show all authors</Trans>
|
||||||
</button>
|
</button>
|
||||||
) : null
|
) : null
|
||||||
// <button
|
// <button
|
||||||
|
@ -1428,7 +1482,7 @@ function Catchup() {
|
||||||
)}
|
)}
|
||||||
<ul
|
<ul
|
||||||
class={`catchup-list catchup-filter-${
|
class={`catchup-list catchup-filter-${
|
||||||
FILTER_VALUES[selectedFilterCategory] || ''
|
selectedFilterCategory || ''
|
||||||
} ${sortBy ? `catchup-sort-${sortBy}` : ''} ${
|
} ${sortBy ? `catchup-sort-${sortBy}` : ''} ${
|
||||||
selectedAuthor && authors[selectedAuthor]
|
selectedAuthor && authors[selectedAuthor]
|
||||||
? `catchup-selected-author`
|
? `catchup-selected-author`
|
||||||
|
@ -1463,9 +1517,9 @@ function Catchup() {
|
||||||
<footer>
|
<footer>
|
||||||
{filteredPosts.length > 5 && (
|
{filteredPosts.length > 5 && (
|
||||||
<p>
|
<p>
|
||||||
{selectedFilterCategory === 'Boosts'
|
{selectedFilterCategory === 'boosts'
|
||||||
? "You don't have to read everything."
|
? t`You don't have to read everything.`
|
||||||
: "That's all."}{' '}
|
: t`That's all.`}{' '}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="textual"
|
class="textual"
|
||||||
|
@ -1473,7 +1527,7 @@ function Catchup() {
|
||||||
scrollableRef.current.scrollTop = 0;
|
scrollableRef.current.scrollTop = 0;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Back to top
|
<Trans>Back to top</Trans>
|
||||||
</button>
|
</button>
|
||||||
.
|
.
|
||||||
</p>
|
</p>
|
||||||
|
@ -1491,47 +1545,117 @@ function Catchup() {
|
||||||
class="sheet-close"
|
class="sheet-close"
|
||||||
onClick={() => setShowHelp(false)}
|
onClick={() => setShowHelp(false)}
|
||||||
>
|
>
|
||||||
<Icon icon="x" />
|
<Icon icon="x" alt={t`Close`} />
|
||||||
</button>
|
</button>
|
||||||
<header>
|
<header>
|
||||||
<h2>Help</h2>
|
<h2>
|
||||||
|
<Trans>Help</Trans>
|
||||||
|
</h2>
|
||||||
</header>
|
</header>
|
||||||
<main>
|
<main>
|
||||||
<dl>
|
<dl>
|
||||||
<dt>Top links</dt>
|
<dt>
|
||||||
|
<Trans>Top links</Trans>
|
||||||
|
</dt>
|
||||||
<dd>
|
<dd>
|
||||||
|
<Trans>
|
||||||
Links shared by followings, sorted by shared counts, boosts
|
Links shared by followings, sorted by shared counts, boosts
|
||||||
and likes.
|
and likes.
|
||||||
|
</Trans>
|
||||||
</dd>
|
</dd>
|
||||||
<dt>Sort: Density</dt>
|
<dt>
|
||||||
|
<Trans>Sort: Density</Trans>
|
||||||
|
</dt>
|
||||||
<dd>
|
<dd>
|
||||||
|
<Trans>
|
||||||
Posts are sorted by information density or depth. Shorter
|
Posts are sorted by information density or depth. Shorter
|
||||||
posts are "lighter" while longer posts are "heavier". Posts
|
posts are "lighter" while longer posts are "heavier". Posts
|
||||||
with photos are "heavier" than posts without photos.
|
with photos are "heavier" than posts without photos.
|
||||||
|
</Trans>
|
||||||
</dd>
|
</dd>
|
||||||
<dt>Group: Authors</dt>
|
<dt>
|
||||||
|
<Trans>Group: Authors</Trans>
|
||||||
|
</dt>
|
||||||
<dd>
|
<dd>
|
||||||
|
<Trans>
|
||||||
Posts are grouped by authors, sorted by posts count per
|
Posts are grouped by authors, sorted by posts count per
|
||||||
author.
|
author.
|
||||||
|
</Trans>
|
||||||
</dd>
|
</dd>
|
||||||
<dt>Keyboard shortcuts</dt>
|
<dt>
|
||||||
<dd>
|
<Trans>Keyboard shortcuts</Trans>
|
||||||
<kbd>j</kbd>: Next post
|
</dt>
|
||||||
|
{/* <dd>
|
||||||
|
<kbd>j</kbd>: <Trans>Next post</Trans>
|
||||||
</dd>
|
</dd>
|
||||||
<dd>
|
<dd>
|
||||||
<kbd>k</kbd>: Previous post
|
<kbd>k</kbd>: <Trans>Previous post</Trans>
|
||||||
</dd>
|
</dd>
|
||||||
<dd>
|
<dd>
|
||||||
<kbd>l</kbd>: Next author
|
<kbd>l</kbd>: <Trans>Next author</Trans>
|
||||||
</dd>
|
</dd>
|
||||||
<dd>
|
<dd>
|
||||||
<kbd>h</kbd>: Previous author
|
<kbd>h</kbd>: <Trans>Previous author</Trans>
|
||||||
</dd>
|
</dd>
|
||||||
<dd>
|
<dd>
|
||||||
<kbd>Enter</kbd>: Open post details
|
<kbd>Enter</kbd>: <Trans>Open post details</Trans>
|
||||||
</dd>
|
</dd>
|
||||||
<dd>
|
<dd>
|
||||||
<kbd>.</kbd>: Scroll to top
|
<kbd>.</kbd>: <Trans>Scroll to top</Trans>
|
||||||
|
</dd> */}
|
||||||
|
<dd>
|
||||||
|
<table>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<Trans>Next post</Trans>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<kbd>j</kbd>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<Trans>Previous post</Trans>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<kbd>k</kbd>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<Trans>Next author</Trans>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<kbd>l</kbd>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<Trans>Previous author</Trans>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<kbd>h</kbd>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<Trans>Open post details</Trans>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<kbd>Enter</kbd>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<Trans>Scroll to top</Trans>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<kbd>.</kbd>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
</dd>
|
</dd>
|
||||||
</dl>
|
</dl>
|
||||||
</main>
|
</main>
|
||||||
|
@ -1713,7 +1837,10 @@ function PostPeek({ post, filterInfo }) {
|
||||||
)}
|
)}
|
||||||
{!!filterInfo ? (
|
{!!filterInfo ? (
|
||||||
<span class="post-peek-filtered">
|
<span class="post-peek-filtered">
|
||||||
Filtered{filterInfo?.titlesStr ? `: ${filterInfo.titlesStr}` : ''}
|
{/* Filtered{filterInfo?.titlesStr ? `: ${filterInfo.titlesStr}` : ''} */}
|
||||||
|
{filterInfo?.titlesStr
|
||||||
|
? t`Filtered: ${filterInfo.titlesStr}`
|
||||||
|
: t`Filtered`}
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
@ -1729,7 +1856,9 @@ function PostPeek({ post, filterInfo }) {
|
||||||
<div class="post-peek-html">
|
<div class="post-peek-html">
|
||||||
{isThread && (
|
{isThread && (
|
||||||
<>
|
<>
|
||||||
<span class="post-peek-tag post-peek-thread">Thread</span>{' '}
|
<span class="post-peek-tag post-peek-thread">
|
||||||
|
<Trans>Thread</Trans>
|
||||||
|
</span>{' '}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{!!content && (
|
{!!content && (
|
||||||
|
@ -1763,7 +1892,7 @@ function PostPeek({ post, filterInfo }) {
|
||||||
{!!poll && (
|
{!!poll && (
|
||||||
<span class="post-peek-tag post-peek-poll">
|
<span class="post-peek-tag post-peek-poll">
|
||||||
<Icon icon="poll" size="s" />
|
<Icon icon="poll" size="s" />
|
||||||
Poll
|
<Trans>Poll</Trans>
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{!!mediaAttachments?.length
|
{!!mediaAttachments?.length
|
||||||
|
@ -1891,32 +2020,26 @@ function PostStats({ post }) {
|
||||||
<span class="post-stats">
|
<span class="post-stats">
|
||||||
{repliesCount > 0 && (
|
{repliesCount > 0 && (
|
||||||
<span class="post-stat-replies">
|
<span class="post-stat-replies">
|
||||||
<Icon icon="comment2" size="s" /> {shortenNumber(repliesCount)}
|
<Icon icon="comment2" size="s" alt={t`Replies`} />{' '}
|
||||||
|
{shortenNumber(repliesCount)}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{favouritesCount > 0 && (
|
{favouritesCount > 0 && (
|
||||||
<span class="post-stat-likes">
|
<span class="post-stat-likes">
|
||||||
<Icon icon="heart" size="s" /> {shortenNumber(favouritesCount)}
|
<Icon icon="heart" size="s" alt={t`Likes`} />{' '}
|
||||||
|
{shortenNumber(favouritesCount)}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{reblogsCount > 0 && (
|
{reblogsCount > 0 && (
|
||||||
<span class="post-stat-boosts">
|
<span class="post-stat-boosts">
|
||||||
<Icon icon="rocket" size="s" /> {shortenNumber(reblogsCount)}
|
<Icon icon="rocket" size="s" alt={t`Boosts`} />{' '}
|
||||||
|
{shortenNumber(reblogsCount)}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { locale } = new Intl.DateTimeFormat().resolvedOptions();
|
|
||||||
const dtf = new Intl.DateTimeFormat(locale, {
|
|
||||||
year: 'numeric',
|
|
||||||
month: 'short',
|
|
||||||
day: 'numeric',
|
|
||||||
hour: 'numeric',
|
|
||||||
minute: 'numeric',
|
|
||||||
});
|
|
||||||
|
|
||||||
function binByTime(data, key, numBins) {
|
function binByTime(data, key, numBins) {
|
||||||
// Extract dates from data objects
|
// Extract dates from data objects
|
||||||
const dates = data.map((item) => new Date(item[key]));
|
const dates = data.map((item) => new Date(item[key]));
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { t, Trans } from '@lingui/macro';
|
||||||
import { useRef } from 'preact/hooks';
|
import { useRef } from 'preact/hooks';
|
||||||
|
|
||||||
import Timeline from '../components/timeline';
|
import Timeline from '../components/timeline';
|
||||||
|
@ -7,7 +8,7 @@ import useTitle from '../utils/useTitle';
|
||||||
const LIMIT = 20;
|
const LIMIT = 20;
|
||||||
|
|
||||||
function Favourites() {
|
function Favourites() {
|
||||||
useTitle('Likes', '/f');
|
useTitle(t`Likes`, '/favourites');
|
||||||
const { masto, instance } = api();
|
const { masto, instance } = api();
|
||||||
const favouritesIterator = useRef();
|
const favouritesIterator = useRef();
|
||||||
async function fetchFavourites(firstLoad) {
|
async function fetchFavourites(firstLoad) {
|
||||||
|
@ -19,10 +20,10 @@ function Favourites() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Timeline
|
<Timeline
|
||||||
title="Likes"
|
title={t`Likes`}
|
||||||
id="favourites"
|
id="favourites"
|
||||||
emptyText="No likes yet. Go like something!"
|
emptyText={`No likes yet. Go like something!`}
|
||||||
errorText="Unable to load likes"
|
errorText={t`Unable to load likes.`}
|
||||||
instance={instance}
|
instance={instance}
|
||||||
fetchItems={fetchFavourites}
|
fetchItems={fetchFavourites}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
import './filters.css';
|
import './filters.css';
|
||||||
|
|
||||||
|
import { i18n } from '@lingui/core';
|
||||||
|
import { msg, Plural, t, Trans } from '@lingui/macro';
|
||||||
|
import { useLingui } from '@lingui/react';
|
||||||
import { useEffect, useReducer, useRef, useState } from 'preact/hooks';
|
import { useEffect, useReducer, useRef, useState } from 'preact/hooks';
|
||||||
|
|
||||||
import Icon from '../components/icon';
|
import Icon from '../components/icon';
|
||||||
|
@ -10,17 +13,18 @@ import Modal from '../components/modal';
|
||||||
import NavMenu from '../components/nav-menu';
|
import NavMenu from '../components/nav-menu';
|
||||||
import RelativeTime from '../components/relative-time';
|
import RelativeTime from '../components/relative-time';
|
||||||
import { api } from '../utils/api';
|
import { api } from '../utils/api';
|
||||||
|
import i18nDuration from '../utils/i18n-duration';
|
||||||
import useInterval from '../utils/useInterval';
|
import useInterval from '../utils/useInterval';
|
||||||
import useTitle from '../utils/useTitle';
|
import useTitle from '../utils/useTitle';
|
||||||
|
|
||||||
const FILTER_CONTEXT = ['home', 'public', 'notifications', 'thread', 'account'];
|
const FILTER_CONTEXT = ['home', 'public', 'notifications', 'thread', 'account'];
|
||||||
const FILTER_CONTEXT_UNIMPLEMENTED = ['notifications', 'thread', 'account'];
|
const FILTER_CONTEXT_UNIMPLEMENTED = ['notifications', 'thread', 'account'];
|
||||||
const FILTER_CONTEXT_LABELS = {
|
const FILTER_CONTEXT_LABELS = {
|
||||||
home: 'Home and lists',
|
home: msg`Home and lists`,
|
||||||
notifications: 'Notifications',
|
notifications: msg`Notifications`,
|
||||||
public: 'Public timelines',
|
public: msg`Public timelines`,
|
||||||
thread: 'Conversations',
|
thread: msg`Conversations`,
|
||||||
account: 'Profiles',
|
account: msg`Profiles`,
|
||||||
};
|
};
|
||||||
|
|
||||||
const EXPIRY_DURATIONS = [
|
const EXPIRY_DURATIONS = [
|
||||||
|
@ -33,20 +37,21 @@ const EXPIRY_DURATIONS = [
|
||||||
60 * 60 * 24 * 7, // 7 days
|
60 * 60 * 24 * 7, // 7 days
|
||||||
60 * 60 * 24 * 30, // 30 days
|
60 * 60 * 24 * 30, // 30 days
|
||||||
];
|
];
|
||||||
|
|
||||||
const EXPIRY_DURATIONS_LABELS = {
|
const EXPIRY_DURATIONS_LABELS = {
|
||||||
0: 'Never',
|
0: msg`Never`,
|
||||||
1800: '30 minutes',
|
1800: i18nDuration(30, 'minute'),
|
||||||
3600: '1 hour',
|
3600: i18nDuration(1, 'hour'),
|
||||||
21600: '6 hours',
|
21600: i18nDuration(6, 'hour'),
|
||||||
43200: '12 hours',
|
43200: i18nDuration(12, 'hour'),
|
||||||
86_400: '24 hours',
|
86_400: i18nDuration(24, 'hour'),
|
||||||
604_800: '7 days',
|
604_800: i18nDuration(7, 'day'),
|
||||||
2_592_000: '30 days',
|
2_592_000: i18nDuration(30, 'day'),
|
||||||
};
|
};
|
||||||
|
|
||||||
function Filters() {
|
function Filters() {
|
||||||
const { masto } = api();
|
const { masto } = api();
|
||||||
useTitle(`Filters`, `/ft`);
|
useTitle(t`Filters`, `/ft`);
|
||||||
const [uiState, setUIState] = useState('default');
|
const [uiState, setUIState] = useState('default');
|
||||||
const [showFiltersAddEditModal, setShowFiltersAddEditModal] = useState(false);
|
const [showFiltersAddEditModal, setShowFiltersAddEditModal] = useState(false);
|
||||||
|
|
||||||
|
@ -81,10 +86,12 @@ function Filters() {
|
||||||
<div class="header-side">
|
<div class="header-side">
|
||||||
<NavMenu />
|
<NavMenu />
|
||||||
<Link to="/" class="button plain">
|
<Link to="/" class="button plain">
|
||||||
<Icon icon="home" size="l" />
|
<Icon icon="home" size="l" alt={t`Home`} />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<h1>Filters</h1>
|
<h1>
|
||||||
|
<Trans>Filters</Trans>
|
||||||
|
</h1>
|
||||||
<div class="header-side">
|
<div class="header-side">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
@ -93,7 +100,7 @@ function Filters() {
|
||||||
setShowFiltersAddEditModal(true);
|
setShowFiltersAddEditModal(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon icon="plus" size="l" alt="New filter" />
|
<Icon icon="plus" size="l" alt={t`New filter`} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -141,8 +148,11 @@ function Filters() {
|
||||||
{filters.length > 1 && (
|
{filters.length > 1 && (
|
||||||
<footer class="ui-state">
|
<footer class="ui-state">
|
||||||
<small class="insignificant">
|
<small class="insignificant">
|
||||||
{filters.length} filter
|
<Plural
|
||||||
{filters.length === 1 ? '' : 's'}
|
value={filters.length}
|
||||||
|
one="# filter"
|
||||||
|
other="# filters"
|
||||||
|
/>
|
||||||
</small>
|
</small>
|
||||||
</footer>
|
</footer>
|
||||||
)}
|
)}
|
||||||
|
@ -152,15 +162,19 @@ function Filters() {
|
||||||
<Loader />
|
<Loader />
|
||||||
</p>
|
</p>
|
||||||
) : uiState === 'error' ? (
|
) : uiState === 'error' ? (
|
||||||
<p class="ui-state">Unable to load filters.</p>
|
<p class="ui-state">
|
||||||
|
<Trans>Unable to load filters.</Trans>
|
||||||
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<p class="ui-state">No filters yet.</p>
|
<p class="ui-state">
|
||||||
|
<Trans>No filters yet.</Trans>
|
||||||
|
</p>
|
||||||
)}
|
)}
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
{!!showFiltersAddEditModal && (
|
{!!showFiltersAddEditModal && (
|
||||||
<Modal
|
<Modal
|
||||||
title="Add filter"
|
title={t`Add filter`}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
setShowFiltersAddEditModal(false);
|
setShowFiltersAddEditModal(false);
|
||||||
}}
|
}}
|
||||||
|
@ -183,6 +197,7 @@ function Filters() {
|
||||||
let _id = 1;
|
let _id = 1;
|
||||||
const incID = () => _id++;
|
const incID = () => _id++;
|
||||||
function FiltersAddEdit({ filter, onClose }) {
|
function FiltersAddEdit({ filter, onClose }) {
|
||||||
|
const { _ } = useLingui();
|
||||||
const { masto } = api();
|
const { masto } = api();
|
||||||
const [uiState, setUIState] = useState('default');
|
const [uiState, setUIState] = useState('default');
|
||||||
const editMode = !!filter;
|
const editMode = !!filter;
|
||||||
|
@ -206,11 +221,11 @@ function FiltersAddEdit({ filter, onClose }) {
|
||||||
<div class="sheet" id="filters-add-edit-modal">
|
<div class="sheet" id="filters-add-edit-modal">
|
||||||
{!!onClose && (
|
{!!onClose && (
|
||||||
<button type="button" class="sheet-close" onClick={onClose}>
|
<button type="button" class="sheet-close" onClick={onClose}>
|
||||||
<Icon icon="x" />
|
<Icon icon="x" alt={t`Close`} />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<header>
|
<header>
|
||||||
<h2>{editMode ? 'Edit filter' : 'New filter'}</h2>
|
<h2>{editMode ? t`Edit filter` : t`New filter`}</h2>
|
||||||
</header>
|
</header>
|
||||||
<main>
|
<main>
|
||||||
<form
|
<form
|
||||||
|
@ -327,8 +342,8 @@ function FiltersAddEdit({ filter, onClose }) {
|
||||||
setUIState('error');
|
setUIState('error');
|
||||||
alert(
|
alert(
|
||||||
editMode
|
editMode
|
||||||
? 'Unable to edit filter'
|
? t`Unable to edit filter`
|
||||||
: 'Unable to create filter',
|
: t`Unable to create filter`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
@ -336,7 +351,9 @@ function FiltersAddEdit({ filter, onClose }) {
|
||||||
>
|
>
|
||||||
<div class="filter-form-row">
|
<div class="filter-form-row">
|
||||||
<label>
|
<label>
|
||||||
<b>Title</b>
|
<b>
|
||||||
|
<Trans>Title</Trans>
|
||||||
|
</b>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
name="title"
|
name="title"
|
||||||
|
@ -376,7 +393,7 @@ function FiltersAddEdit({ filter, onClose }) {
|
||||||
defaultChecked={wholeWord}
|
defaultChecked={wholeWord}
|
||||||
disabled={uiState === 'loading'}
|
disabled={uiState === 'loading'}
|
||||||
/>{' '}
|
/>{' '}
|
||||||
Whole word
|
<Trans>Whole word</Trans>
|
||||||
</label>
|
</label>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
@ -392,7 +409,7 @@ function FiltersAddEdit({ filter, onClose }) {
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon icon="x" />
|
<Icon icon="x" alt={t`Remove`} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
@ -401,7 +418,9 @@ function FiltersAddEdit({ filter, onClose }) {
|
||||||
</ul>
|
</ul>
|
||||||
) : (
|
) : (
|
||||||
<div class="filter-keywords">
|
<div class="filter-keywords">
|
||||||
<div class="insignificant">No keywords. Add one.</div>
|
<div class="insignificant">
|
||||||
|
<Trans>No keywords. Add one.</Trans>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<footer class="filter-keywords-footer">
|
<footer class="filter-keywords-footer">
|
||||||
|
@ -427,12 +446,15 @@ function FiltersAddEdit({ filter, onClose }) {
|
||||||
}, 10);
|
}, 10);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Add keyword
|
<Trans>Add keyword</Trans>
|
||||||
</button>{' '}
|
</button>{' '}
|
||||||
{filteredEditKeywords?.length > 1 && (
|
{filteredEditKeywords?.length > 1 && (
|
||||||
<small class="insignificant">
|
<small class="insignificant">
|
||||||
{filteredEditKeywords.length} keyword
|
<Plural
|
||||||
{filteredEditKeywords.length === 1 ? '' : 's'}
|
value={filteredEditKeywords.length}
|
||||||
|
one="# keyword"
|
||||||
|
other="# keywords"
|
||||||
|
/>
|
||||||
</small>
|
</small>
|
||||||
)}
|
)}
|
||||||
</footer>
|
</footer>
|
||||||
|
@ -440,7 +462,9 @@ function FiltersAddEdit({ filter, onClose }) {
|
||||||
<div class="filter-form-cols">
|
<div class="filter-form-cols">
|
||||||
<div class="filter-form-col">
|
<div class="filter-form-col">
|
||||||
<div>
|
<div>
|
||||||
<b>Filter from…</b>
|
<b>
|
||||||
|
<Trans>Filter from…</Trans>
|
||||||
|
</b>
|
||||||
</div>
|
</div>
|
||||||
{FILTER_CONTEXT.map((ctx) => (
|
{FILTER_CONTEXT.map((ctx) => (
|
||||||
<div>
|
<div>
|
||||||
|
@ -458,27 +482,29 @@ function FiltersAddEdit({ filter, onClose }) {
|
||||||
defaultChecked={!!context ? context.includes(ctx) : true}
|
defaultChecked={!!context ? context.includes(ctx) : true}
|
||||||
disabled={uiState === 'loading'}
|
disabled={uiState === 'loading'}
|
||||||
/>{' '}
|
/>{' '}
|
||||||
{FILTER_CONTEXT_LABELS[ctx]}
|
{_(FILTER_CONTEXT_LABELS[ctx])}
|
||||||
{FILTER_CONTEXT_UNIMPLEMENTED.includes(ctx) ? '*' : ''}
|
{FILTER_CONTEXT_UNIMPLEMENTED.includes(ctx) ? '*' : ''}
|
||||||
</label>{' '}
|
</label>{' '}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
<p>
|
<p>
|
||||||
<small class="insignificant">* Not implemented yet</small>
|
<small class="insignificant">
|
||||||
|
<Trans>* Not implemented yet</Trans>
|
||||||
|
</small>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="filter-form-col">
|
<div class="filter-form-col">
|
||||||
{editMode && (
|
{editMode && (
|
||||||
<>
|
<Trans>
|
||||||
Status:{' '}
|
Status:{' '}
|
||||||
<b>
|
<b>
|
||||||
<ExpiryStatus expiresAt={expiresAt} showNeverExpires />
|
<ExpiryStatus expiresAt={expiresAt} showNeverExpires />
|
||||||
</b>
|
</b>
|
||||||
</>
|
</Trans>
|
||||||
)}
|
)}
|
||||||
<div>
|
<div>
|
||||||
<label for="filters-expires_in">
|
<label for="filters-expires_in">
|
||||||
{editMode ? 'Change expiry' : 'Expiry'}
|
{editMode ? t`Change expiry` : t`Expiry`}
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
id="filters-expires_in"
|
id="filters-expires_in"
|
||||||
|
@ -488,12 +514,16 @@ function FiltersAddEdit({ filter, onClose }) {
|
||||||
>
|
>
|
||||||
{editMode && <option></option>}
|
{editMode && <option></option>}
|
||||||
{EXPIRY_DURATIONS.map((v) => (
|
{EXPIRY_DURATIONS.map((v) => (
|
||||||
<option value={v}>{EXPIRY_DURATIONS_LABELS[v]}</option>
|
<option value={v}>
|
||||||
|
{typeof EXPIRY_DURATIONS_LABELS[v] === 'function'
|
||||||
|
? EXPIRY_DURATIONS_LABELS[v]()
|
||||||
|
: _(EXPIRY_DURATIONS_LABELS[v])}
|
||||||
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<p>
|
<p>
|
||||||
Filtered post will be…
|
<Trans>Filtered post will be…</Trans>
|
||||||
<br />
|
<br />
|
||||||
<label class="ib">
|
<label class="ib">
|
||||||
<input
|
<input
|
||||||
|
@ -503,7 +533,7 @@ function FiltersAddEdit({ filter, onClose }) {
|
||||||
defaultChecked={filterAction === 'warn' || !editMode}
|
defaultChecked={filterAction === 'warn' || !editMode}
|
||||||
disabled={uiState === 'loading'}
|
disabled={uiState === 'loading'}
|
||||||
/>{' '}
|
/>{' '}
|
||||||
minimized
|
<Trans>minimized</Trans>
|
||||||
</label>{' '}
|
</label>{' '}
|
||||||
<label class="ib">
|
<label class="ib">
|
||||||
<input
|
<input
|
||||||
|
@ -513,7 +543,7 @@ function FiltersAddEdit({ filter, onClose }) {
|
||||||
defaultChecked={filterAction === 'hide'}
|
defaultChecked={filterAction === 'hide'}
|
||||||
disabled={uiState === 'loading'}
|
disabled={uiState === 'loading'}
|
||||||
/>{' '}
|
/>{' '}
|
||||||
hidden
|
<Trans>hidden</Trans>
|
||||||
</label>
|
</label>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -521,7 +551,7 @@ function FiltersAddEdit({ filter, onClose }) {
|
||||||
<footer class="filter-form-footer">
|
<footer class="filter-form-footer">
|
||||||
<span>
|
<span>
|
||||||
<button type="submit" disabled={uiState === 'loading'}>
|
<button type="submit" disabled={uiState === 'loading'}>
|
||||||
{editMode ? 'Save' : 'Create'}
|
{editMode ? t`Save` : t`Create`}
|
||||||
</button>{' '}
|
</button>{' '}
|
||||||
<Loader abrupt hidden={uiState !== 'loading'} />
|
<Loader abrupt hidden={uiState !== 'loading'} />
|
||||||
</span>
|
</span>
|
||||||
|
@ -530,7 +560,7 @@ function FiltersAddEdit({ filter, onClose }) {
|
||||||
disabled={uiState === 'loading'}
|
disabled={uiState === 'loading'}
|
||||||
align="end"
|
align="end"
|
||||||
menuItemClassName="danger"
|
menuItemClassName="danger"
|
||||||
confirmLabel="Delete this filter?"
|
confirmLabel={t`Delete this filter?`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setUIState('loading');
|
setUIState('loading');
|
||||||
(async () => {
|
(async () => {
|
||||||
|
@ -543,7 +573,7 @@ function FiltersAddEdit({ filter, onClose }) {
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
setUIState('error');
|
setUIState('error');
|
||||||
alert('Unable to delete filter.');
|
alert(t`Unable to delete filter.`);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
}}
|
}}
|
||||||
|
@ -554,7 +584,7 @@ function FiltersAddEdit({ filter, onClose }) {
|
||||||
onClick={() => {}}
|
onClick={() => {}}
|
||||||
disabled={uiState === 'loading'}
|
disabled={uiState === 'loading'}
|
||||||
>
|
>
|
||||||
Delete…
|
<Trans>Delete…</Trans>
|
||||||
</button>
|
</button>
|
||||||
</MenuConfirm>
|
</MenuConfirm>
|
||||||
)}
|
)}
|
||||||
|
@ -575,13 +605,13 @@ function ExpiryStatus({ expiresAt, showNeverExpires }) {
|
||||||
useInterval(rerender, expired || 30_000);
|
useInterval(rerender, expired || 30_000);
|
||||||
|
|
||||||
return expired ? (
|
return expired ? (
|
||||||
'Expired'
|
t`Expired`
|
||||||
) : hasExpiry ? (
|
) : hasExpiry ? (
|
||||||
<>
|
<Trans>
|
||||||
Expiring <RelativeTime datetime={expiresAtDate} />
|
Expiring <RelativeTime datetime={expiresAtDate} />
|
||||||
</>
|
</Trans>
|
||||||
) : (
|
) : (
|
||||||
showNeverExpires && 'Never expires'
|
showNeverExpires && t`Never expires`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { Plural, t, Trans } from '@lingui/macro';
|
||||||
import { useEffect, useState } from 'preact/hooks';
|
import { useEffect, useState } from 'preact/hooks';
|
||||||
|
|
||||||
import Icon from '../components/icon';
|
import Icon from '../components/icon';
|
||||||
|
@ -10,7 +11,7 @@ import useTitle from '../utils/useTitle';
|
||||||
|
|
||||||
function FollowedHashtags() {
|
function FollowedHashtags() {
|
||||||
const { masto, instance } = api();
|
const { masto, instance } = api();
|
||||||
useTitle(`Followed Hashtags`, `/fh`);
|
useTitle(t`Followed Hashtags`, `/fh`);
|
||||||
const [uiState, setUIState] = useState('default');
|
const [uiState, setUIState] = useState('default');
|
||||||
|
|
||||||
const [followedHashtags, setFollowedHashtags] = useState([]);
|
const [followedHashtags, setFollowedHashtags] = useState([]);
|
||||||
|
@ -36,10 +37,12 @@ function FollowedHashtags() {
|
||||||
<div class="header-side">
|
<div class="header-side">
|
||||||
<NavMenu />
|
<NavMenu />
|
||||||
<Link to="/" class="button plain">
|
<Link to="/" class="button plain">
|
||||||
<Icon icon="home" size="l" />
|
<Icon icon="home" size="l" alt={t`Home`} />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<h1>Followed Hashtags</h1>
|
<h1>
|
||||||
|
<Trans>Followed Hashtags</Trans>
|
||||||
|
</h1>
|
||||||
<div class="header-side" />
|
<div class="header-side" />
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
@ -56,7 +59,7 @@ function FollowedHashtags() {
|
||||||
: `/t/${tag.name}`
|
: `/t/${tag.name}`
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Icon icon="hashtag" /> <span>{tag.name}</span>
|
<Icon icon="hashtag" alt="#" /> <span>{tag.name}</span>
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
|
@ -64,8 +67,11 @@ function FollowedHashtags() {
|
||||||
{followedHashtags.length > 1 && (
|
{followedHashtags.length > 1 && (
|
||||||
<footer class="ui-state">
|
<footer class="ui-state">
|
||||||
<small class="insignificant">
|
<small class="insignificant">
|
||||||
{followedHashtags.length} hashtag
|
<Plural
|
||||||
{followedHashtags.length === 1 ? '' : 's'}
|
value={followedHashtags.length}
|
||||||
|
one="# hashtag"
|
||||||
|
other="# hashtags"
|
||||||
|
/>
|
||||||
</small>
|
</small>
|
||||||
</footer>
|
</footer>
|
||||||
)}
|
)}
|
||||||
|
@ -75,9 +81,13 @@ function FollowedHashtags() {
|
||||||
<Loader abrupt />
|
<Loader abrupt />
|
||||||
</p>
|
</p>
|
||||||
) : uiState === 'error' ? (
|
) : uiState === 'error' ? (
|
||||||
<p class="ui-state">Unable to load followed hashtags.</p>
|
<p class="ui-state">
|
||||||
|
<Trans>Unable to load followed hashtags.</Trans>
|
||||||
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<p class="ui-state">No hashtags followed yet.</p>
|
<p class="ui-state">
|
||||||
|
<Trans>No hashtags followed yet.</Trans>
|
||||||
|
</p>
|
||||||
)}
|
)}
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { t, Trans } from '@lingui/macro';
|
||||||
import { useEffect, useRef } from 'preact/hooks';
|
import { useEffect, useRef } from 'preact/hooks';
|
||||||
import { useSnapshot } from 'valtio';
|
import { useSnapshot } from 'valtio';
|
||||||
|
|
||||||
|
@ -16,7 +17,7 @@ import useTitle from '../utils/useTitle';
|
||||||
const LIMIT = 20;
|
const LIMIT = 20;
|
||||||
|
|
||||||
function Following({ title, path, id, ...props }) {
|
function Following({ title, path, id, ...props }) {
|
||||||
useTitle(title || 'Following', path || '/following');
|
useTitle(title || t`Following`, path || '/following');
|
||||||
const { masto, streaming, instance } = api();
|
const { masto, streaming, instance } = api();
|
||||||
const snapStates = useSnapshot(states);
|
const snapStates = useSnapshot(states);
|
||||||
const homeIterator = useRef();
|
const homeIterator = useRef();
|
||||||
|
@ -127,10 +128,10 @@ function Following({ title, path, id, ...props }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Timeline
|
<Timeline
|
||||||
title={title || 'Following'}
|
title={title || t`Following`}
|
||||||
id={id || 'following'}
|
id={id || 'following'}
|
||||||
emptyText="Nothing to see here."
|
emptyText={t`Nothing to see here.`}
|
||||||
errorText="Unable to load posts."
|
errorText={t`Unable to load posts.`}
|
||||||
instance={instance}
|
instance={instance}
|
||||||
fetchItems={fetchHome}
|
fetchItems={fetchHome}
|
||||||
checkForUpdates={checkForUpdates}
|
checkForUpdates={checkForUpdates}
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { plural, t, Trans } from '@lingui/macro';
|
||||||
|
import { useLingui } from '@lingui/react';
|
||||||
import {
|
import {
|
||||||
FocusableItem,
|
FocusableItem,
|
||||||
MenuDivider,
|
MenuDivider,
|
||||||
|
@ -48,10 +50,13 @@ function Hashtags({ media: mediaView, columnMode, ...props }) {
|
||||||
authenticated: currentAuthenticated,
|
authenticated: currentAuthenticated,
|
||||||
} = api();
|
} = api();
|
||||||
const hashtagTitle = hashtags.map((t) => `#${t}`).join(' ');
|
const hashtagTitle = hashtags.map((t) => `#${t}`).join(' ');
|
||||||
const hashtagPostTitle = media ? ` (Media only)` : '';
|
|
||||||
const title = instance
|
const title = instance
|
||||||
? `${hashtagTitle}${hashtagPostTitle} on ${instance}`
|
? media
|
||||||
: `${hashtagTitle}${hashtagPostTitle}`;
|
? t`${hashtagTitle} (Media only) on ${instance}`
|
||||||
|
: t`${hashtagTitle} on ${instance}`
|
||||||
|
: media
|
||||||
|
? t`${hashtagTitle} (Media only)`
|
||||||
|
: t`${hashtagTitle}`;
|
||||||
useTitle(title, `/:instance?/t/:hashtag`);
|
useTitle(title, `/:instance?/t/:hashtag`);
|
||||||
const latestItem = useRef();
|
const latestItem = useRef();
|
||||||
|
|
||||||
|
@ -173,8 +178,8 @@ function Hashtags({ media: mediaView, columnMode, ...props }) {
|
||||||
}
|
}
|
||||||
id="hashtag"
|
id="hashtag"
|
||||||
instance={instance}
|
instance={instance}
|
||||||
emptyText="No one has posted anything with this tag yet."
|
emptyText={t`No one has posted anything with this tag yet.`}
|
||||||
errorText="Unable to load posts with this tag"
|
errorText={t`Unable to load posts with this tag`}
|
||||||
fetchItems={fetchHashtags}
|
fetchItems={fetchHashtags}
|
||||||
checkForUpdates={checkForUpdates}
|
checkForUpdates={checkForUpdates}
|
||||||
useItemID
|
useItemID
|
||||||
|
@ -191,7 +196,7 @@ function Hashtags({ media: mediaView, columnMode, ...props }) {
|
||||||
position="anchor"
|
position="anchor"
|
||||||
menuButton={
|
menuButton={
|
||||||
<button type="button" class="plain">
|
<button type="button" class="plain">
|
||||||
<Icon icon="more" size="l" />
|
<Icon icon="more" size="l" alt={t`More`} />
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
@ -215,7 +220,7 @@ function Hashtags({ media: mediaView, columnMode, ...props }) {
|
||||||
.unfollow()
|
.unfollow()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
setInfo({ ...info, following: false });
|
setInfo({ ...info, following: false });
|
||||||
showToast(`Unfollowed #${hashtag}`);
|
showToast(t`Unfollowed #${hashtag}`);
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
alert(e);
|
alert(e);
|
||||||
|
@ -230,7 +235,7 @@ function Hashtags({ media: mediaView, columnMode, ...props }) {
|
||||||
.follow()
|
.follow()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
setInfo({ ...info, following: true });
|
setInfo({ ...info, following: true });
|
||||||
showToast(`Followed #${hashtag}`);
|
showToast(t`Followed #${hashtag}`);
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
alert(e);
|
alert(e);
|
||||||
|
@ -244,11 +249,17 @@ function Hashtags({ media: mediaView, columnMode, ...props }) {
|
||||||
>
|
>
|
||||||
{info.following ? (
|
{info.following ? (
|
||||||
<>
|
<>
|
||||||
<Icon icon="check-circle" /> <span>Following…</span>
|
<Icon icon="check-circle" />{' '}
|
||||||
|
<span>
|
||||||
|
<Trans>Following…</Trans>
|
||||||
|
</span>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Icon icon="plus" /> <span>Follow</span>
|
<Icon icon="plus" />{' '}
|
||||||
|
<span>
|
||||||
|
<Trans>Follow</Trans>
|
||||||
|
</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</MenuConfirm>
|
</MenuConfirm>
|
||||||
|
@ -268,7 +279,7 @@ function Hashtags({ media: mediaView, columnMode, ...props }) {
|
||||||
.remove()
|
.remove()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
setIsFeaturedTag(false);
|
setIsFeaturedTag(false);
|
||||||
showToast('Unfeatured on profile');
|
showToast(t`Unfeatured on profile`);
|
||||||
setFeaturedTags(
|
setFeaturedTags(
|
||||||
featuredTags.filter(
|
featuredTags.filter(
|
||||||
(tag) => tag.id !== featuredTagID,
|
(tag) => tag.id !== featuredTagID,
|
||||||
|
@ -282,7 +293,7 @@ function Hashtags({ media: mediaView, columnMode, ...props }) {
|
||||||
setFeaturedUIState('default');
|
setFeaturedUIState('default');
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
showToast('Unable to unfeature on profile');
|
showToast(t`Unable to unfeature on profile`);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
masto.v1.featuredTags
|
masto.v1.featuredTags
|
||||||
|
@ -291,7 +302,7 @@ function Hashtags({ media: mediaView, columnMode, ...props }) {
|
||||||
})
|
})
|
||||||
.then((value) => {
|
.then((value) => {
|
||||||
setIsFeaturedTag(true);
|
setIsFeaturedTag(true);
|
||||||
showToast('Featured on profile');
|
showToast(t`Featured on profile`);
|
||||||
setFeaturedTags(featuredTags.concat(value));
|
setFeaturedTags(featuredTags.concat(value));
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
|
@ -306,12 +317,16 @@ function Hashtags({ media: mediaView, columnMode, ...props }) {
|
||||||
{isFeaturedTag ? (
|
{isFeaturedTag ? (
|
||||||
<>
|
<>
|
||||||
<Icon icon="check-circle" />
|
<Icon icon="check-circle" />
|
||||||
<span>Featured on profile</span>
|
<span>
|
||||||
|
<Trans>Featured on profile</Trans>
|
||||||
|
</span>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Icon icon="check-circle" />
|
<Icon icon="check-circle" />
|
||||||
<span>Feature on profile</span>
|
<span>
|
||||||
|
<Trans>Feature on profile</Trans>
|
||||||
|
</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
@ -320,7 +335,9 @@ function Hashtags({ media: mediaView, columnMode, ...props }) {
|
||||||
)}
|
)}
|
||||||
{!mediaFirst && (
|
{!mediaFirst && (
|
||||||
<>
|
<>
|
||||||
<MenuHeader className="plain">Filters</MenuHeader>
|
<MenuHeader className="plain">
|
||||||
|
<Trans>Filters</Trans>
|
||||||
|
</MenuHeader>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={!!media}
|
checked={!!media}
|
||||||
|
@ -333,8 +350,10 @@ function Hashtags({ media: mediaView, columnMode, ...props }) {
|
||||||
setSearchParams(searchParams);
|
setSearchParams(searchParams);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon icon="check-circle" />{' '}
|
<Icon icon="check-circle" alt="☑️" />{' '}
|
||||||
<span class="menu-grow">Media only</span>
|
<span class="menu-grow">
|
||||||
|
<Trans>Media only</Trans>
|
||||||
|
</span>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuDivider />
|
<MenuDivider />
|
||||||
</>
|
</>
|
||||||
|
@ -370,7 +389,11 @@ function Hashtags({ media: mediaView, columnMode, ...props }) {
|
||||||
ref={ref}
|
ref={ref}
|
||||||
type="text"
|
type="text"
|
||||||
placeholder={
|
placeholder={
|
||||||
reachLimit ? `Max ${TOTAL_TAGS_LIMIT} tags` : 'Add hashtag'
|
reachLimit
|
||||||
|
? plural(TOTAL_TAGS_LIMIT, {
|
||||||
|
other: 'Max # tags',
|
||||||
|
})
|
||||||
|
: t`Add hashtag`
|
||||||
}
|
}
|
||||||
required
|
required
|
||||||
autocorrect="off"
|
autocorrect="off"
|
||||||
|
@ -385,9 +408,9 @@ function Hashtags({ media: mediaView, columnMode, ...props }) {
|
||||||
)}
|
)}
|
||||||
</FocusableItem>
|
</FocusableItem>
|
||||||
<MenuGroup takeOverflow>
|
<MenuGroup takeOverflow>
|
||||||
{hashtags.map((t, i) => (
|
{hashtags.map((tag, i) => (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
key={t}
|
key={tag}
|
||||||
disabled={hashtags.length === 1}
|
disabled={hashtags.length === 1}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
hashtags.splice(i, 1);
|
hashtags.splice(i, 1);
|
||||||
|
@ -402,10 +425,10 @@ function Hashtags({ media: mediaView, columnMode, ...props }) {
|
||||||
: `/t/${hashtags.join('+')}${linkParams}`;
|
: `/t/${hashtags.join('+')}${linkParams}`;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon icon="x" alt="Remove hashtag" class="danger-icon" />
|
<Icon icon="x" alt={t`Remove hashtag`} class="danger-icon" />
|
||||||
<span class="bidi-isolate">
|
<span class="bidi-isolate">
|
||||||
<span class="more-insignificant">#</span>
|
<span class="more-insignificant">#</span>
|
||||||
{t}
|
{tag}
|
||||||
</span>
|
</span>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
))}
|
))}
|
||||||
|
@ -416,7 +439,10 @@ function Hashtags({ media: mediaView, columnMode, ...props }) {
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (states.shortcuts.length >= SHORTCUTS_LIMIT) {
|
if (states.shortcuts.length >= SHORTCUTS_LIMIT) {
|
||||||
alert(
|
alert(
|
||||||
`Max ${SHORTCUTS_LIMIT} shortcuts reached. Unable to add shortcut.`,
|
plural(SHORTCUTS_LIMIT, {
|
||||||
|
one: 'Max # shortcut reached. Unable to add shortcut.',
|
||||||
|
other: 'Max # shortcuts reached. Unable to add shortcut.',
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -442,22 +468,25 @@ function Hashtags({ media: mediaView, columnMode, ...props }) {
|
||||||
(s.media ? !!s.media === !!shortcut.media : true),
|
(s.media ? !!s.media === !!shortcut.media : true),
|
||||||
);
|
);
|
||||||
if (exists) {
|
if (exists) {
|
||||||
alert('This shortcut already exists');
|
alert(t`This shortcut already exists`);
|
||||||
} else {
|
} else {
|
||||||
states.shortcuts.push(shortcut);
|
states.shortcuts.push(shortcut);
|
||||||
showToast(`Hashtag shortcut added`);
|
showToast(t`Hashtag shortcut added`);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon icon="shortcut" /> <span>Add to Shortcuts</span>
|
<Icon icon="shortcut" />{' '}
|
||||||
|
<span>
|
||||||
|
<Trans>Add to Shortcuts</Trans>
|
||||||
|
</span>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
let newInstance = prompt(
|
let newInstance = prompt(
|
||||||
'Enter a new instance e.g. "mastodon.social"',
|
t`Enter a new instance e.g. "mastodon.social"`,
|
||||||
);
|
);
|
||||||
if (!/\./.test(newInstance)) {
|
if (!/\./.test(newInstance)) {
|
||||||
if (newInstance) alert('Invalid instance');
|
if (newInstance) alert(t`Invalid instance`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (newInstance) {
|
if (newInstance) {
|
||||||
|
@ -469,7 +498,10 @@ function Hashtags({ media: mediaView, columnMode, ...props }) {
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon icon="bus" /> <span>Go to another instance…</span>
|
<Icon icon="bus" />{' '}
|
||||||
|
<span>
|
||||||
|
<Trans>Go to another instance…</Trans>
|
||||||
|
</span>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
{currentInstance !== instance && (
|
{currentInstance !== instance && (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
|
@ -481,7 +513,9 @@ function Hashtags({ media: mediaView, columnMode, ...props }) {
|
||||||
>
|
>
|
||||||
<Icon icon="bus" />{' '}
|
<Icon icon="bus" />{' '}
|
||||||
<small class="menu-double-lines">
|
<small class="menu-double-lines">
|
||||||
|
<Trans>
|
||||||
Go to my instance (<b>{currentInstance}</b>)
|
Go to my instance (<b>{currentInstance}</b>)
|
||||||
|
</Trans>
|
||||||
</small>
|
</small>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import './notifications-menu.css';
|
import './notifications-menu.css';
|
||||||
|
|
||||||
|
import { t, Trans } from '@lingui/macro';
|
||||||
import { ControlledMenu } from '@szhsin/react-menu';
|
import { ControlledMenu } from '@szhsin/react-menu';
|
||||||
import { memo } from 'preact/compat';
|
import { memo } from 'preact/compat';
|
||||||
import { useEffect, useRef, useState } from 'preact/hooks';
|
import { useEffect, useRef, useState } from 'preact/hooks';
|
||||||
|
@ -46,7 +47,7 @@ function Home() {
|
||||||
<Columns />
|
<Columns />
|
||||||
) : (
|
) : (
|
||||||
<Following
|
<Following
|
||||||
title="Home"
|
title={t`Home`}
|
||||||
path="/"
|
path="/"
|
||||||
id="home"
|
id="home"
|
||||||
headerStart={false}
|
headerStart={false}
|
||||||
|
@ -77,7 +78,7 @@ function NotificationsLink() {
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon icon="notification" size="l" alt="Notifications" />
|
<Icon icon="notification" size="l" alt={t`Notifications`} />
|
||||||
</Link>
|
</Link>
|
||||||
<NotificationsMenu
|
<NotificationsMenu
|
||||||
state={menuState}
|
state={menuState}
|
||||||
|
@ -176,7 +177,9 @@ function NotificationsMenu({ anchorRef, state, onClose }) {
|
||||||
boundingBoxPadding="8 8 8 8"
|
boundingBoxPadding="8 8 8 8"
|
||||||
>
|
>
|
||||||
<header>
|
<header>
|
||||||
<h2>Notifications</h2>
|
<h2>
|
||||||
|
<Trans>Notifications</Trans>
|
||||||
|
</h2>
|
||||||
</header>
|
</header>
|
||||||
<main>
|
<main>
|
||||||
{snapStates.notifications.length ? (
|
{snapStates.notifications.length ? (
|
||||||
|
@ -199,10 +202,12 @@ function NotificationsMenu({ anchorRef, state, onClose }) {
|
||||||
) : (
|
) : (
|
||||||
uiState === 'error' && (
|
uiState === 'error' && (
|
||||||
<div class="ui-state">
|
<div class="ui-state">
|
||||||
<p>Unable to fetch notifications.</p>
|
<p>
|
||||||
|
<Trans>Unable to fetch notifications.</Trans>
|
||||||
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<button type="button" onClick={loadNotifications}>
|
<button type="button" onClick={loadNotifications}>
|
||||||
Try again
|
<Trans>Try again</Trans>
|
||||||
</button>
|
</button>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -211,16 +216,21 @@ function NotificationsMenu({ anchorRef, state, onClose }) {
|
||||||
</main>
|
</main>
|
||||||
<footer>
|
<footer>
|
||||||
<Link to="/mentions" class="button plain">
|
<Link to="/mentions" class="button plain">
|
||||||
<Icon icon="at" /> <span>Mentions</span>
|
<Icon icon="at" />{' '}
|
||||||
|
<span>
|
||||||
|
<Trans>Mentions</Trans>
|
||||||
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
<Link to="/notifications" class="button plain2">
|
<Link to="/notifications" class="button plain2">
|
||||||
{hasFollowRequests ? (
|
{hasFollowRequests ? (
|
||||||
<>
|
<Trans>
|
||||||
<span class="tag collapsed">New</span>{' '}
|
<span class="tag collapsed">New</span>{' '}
|
||||||
<span>Follow Requests</span>
|
<span>Follow Requests</span>
|
||||||
</>
|
</Trans>
|
||||||
) : (
|
) : (
|
||||||
<b>See all</b>
|
<b>
|
||||||
|
<Trans>See all</Trans>
|
||||||
|
</b>
|
||||||
)}{' '}
|
)}{' '}
|
||||||
<Icon icon="arrow-right" />
|
<Icon icon="arrow-right" />
|
||||||
</Link>
|
</Link>
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { t, Trans } from '@lingui/macro';
|
||||||
import { useLayoutEffect, useState } from 'preact/hooks';
|
import { useLayoutEffect, useState } from 'preact/hooks';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
|
@ -63,7 +64,9 @@ export default function HttpRoute() {
|
||||||
{uiState === 'loading' ? (
|
{uiState === 'loading' ? (
|
||||||
<>
|
<>
|
||||||
<Loader abrupt />
|
<Loader abrupt />
|
||||||
<h2>Resolving…</h2>
|
<h2>
|
||||||
|
<Trans>Resolving…</Trans>
|
||||||
|
</h2>
|
||||||
<p>
|
<p>
|
||||||
<a href={url} target="_blank" rel="noopener noreferrer">
|
<a href={url} target="_blank" rel="noopener noreferrer">
|
||||||
{url}
|
{url}
|
||||||
|
@ -72,7 +75,9 @@ export default function HttpRoute() {
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<h2>Unable to resolve URL</h2>
|
<h2>
|
||||||
|
<Trans>Unable to resolve URL</Trans>
|
||||||
|
</h2>
|
||||||
<p>
|
<p>
|
||||||
<a href={url} target="_blank" rel="noopener noreferrer">
|
<a href={url} target="_blank" rel="noopener noreferrer">
|
||||||
{url}
|
{url}
|
||||||
|
@ -82,7 +87,9 @@ export default function HttpRoute() {
|
||||||
)}
|
)}
|
||||||
<hr />
|
<hr />
|
||||||
<p>
|
<p>
|
||||||
<Link to="/">Go home</Link>
|
<Link to="/">
|
||||||
|
<Trans>Go home</Trans>
|
||||||
|
</Link>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import './lists.css';
|
import './lists.css';
|
||||||
|
|
||||||
|
import { t, Trans } from '@lingui/macro';
|
||||||
import { Menu, MenuDivider, MenuItem } from '@szhsin/react-menu';
|
import { Menu, MenuDivider, MenuItem } from '@szhsin/react-menu';
|
||||||
import { useEffect, useRef, useState } from 'preact/hooks';
|
import { useEffect, useRef, useState } from 'preact/hooks';
|
||||||
import { InView } from 'react-intersection-observer';
|
import { InView } from 'react-intersection-observer';
|
||||||
|
@ -103,8 +104,8 @@ function List(props) {
|
||||||
key={id}
|
key={id}
|
||||||
title={list.title}
|
title={list.title}
|
||||||
id="list"
|
id="list"
|
||||||
emptyText="Nothing yet."
|
emptyText={t`Nothing yet.`}
|
||||||
errorText="Unable to load posts."
|
errorText={t`Unable to load posts.`}
|
||||||
instance={instance}
|
instance={instance}
|
||||||
fetchItems={fetchList}
|
fetchItems={fetchList}
|
||||||
checkForUpdates={checkForUpdates}
|
checkForUpdates={checkForUpdates}
|
||||||
|
@ -122,13 +123,15 @@ function List(props) {
|
||||||
overflow="auto"
|
overflow="auto"
|
||||||
menuButton={
|
menuButton={
|
||||||
<button type="button" class="plain">
|
<button type="button" class="plain">
|
||||||
<Icon icon="list" size="l" alt="Lists" />
|
<Icon icon="list" size="l" alt={t`Lists`} />
|
||||||
<Icon icon="chevron-down" size="s" />
|
<Icon icon="chevron-down" size="s" />
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<MenuLink to="/l">
|
<MenuLink to="/l">
|
||||||
<span>All Lists</span>
|
<span>
|
||||||
|
<Trans>All Lists</Trans>
|
||||||
|
</span>
|
||||||
</MenuLink>
|
</MenuLink>
|
||||||
{lists?.length > 0 && (
|
{lists?.length > 0 && (
|
||||||
<>
|
<>
|
||||||
|
@ -151,7 +154,7 @@ function List(props) {
|
||||||
position="anchor"
|
position="anchor"
|
||||||
menuButton={
|
menuButton={
|
||||||
<button type="button" class="plain">
|
<button type="button" class="plain">
|
||||||
<Icon icon="more" size="l" />
|
<Icon icon="more" size="l" alt={t`More`} />
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
@ -163,11 +166,15 @@ function List(props) {
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Icon icon="pencil" size="l" />
|
<Icon icon="pencil" size="l" />
|
||||||
<span>Edit</span>
|
<span>
|
||||||
|
<Trans>Edit</Trans>
|
||||||
|
</span>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem onClick={() => setShowManageMembersModal(true)}>
|
<MenuItem onClick={() => setShowManageMembersModal(true)}>
|
||||||
<Icon icon="group" size="l" />
|
<Icon icon="group" size="l" />
|
||||||
<span>Manage members</span>
|
<span>
|
||||||
|
<Trans>Manage members</Trans>
|
||||||
|
</span>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</Menu2>
|
</Menu2>
|
||||||
}
|
}
|
||||||
|
@ -264,11 +271,13 @@ function ListManageMembers({ listID, onClose }) {
|
||||||
<div class="sheet" id="list-manage-members-container">
|
<div class="sheet" id="list-manage-members-container">
|
||||||
{!!onClose && (
|
{!!onClose && (
|
||||||
<button type="button" class="sheet-close" onClick={onClose}>
|
<button type="button" class="sheet-close" onClick={onClose}>
|
||||||
<Icon icon="x" />
|
<Icon icon="x" alt={t`Close`} />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<header>
|
<header>
|
||||||
<h2>Manage members</h2>
|
<h2>
|
||||||
|
<Trans>Manage members</Trans>
|
||||||
|
</h2>
|
||||||
</header>
|
</header>
|
||||||
<main>
|
<main>
|
||||||
<ul>
|
<ul>
|
||||||
|
@ -281,7 +290,7 @@ function ListManageMembers({ listID, onClose }) {
|
||||||
{showMore && uiState === 'default' && (
|
{showMore && uiState === 'default' && (
|
||||||
<InView as="li" onChange={(inView) => inView && fetchMembers()}>
|
<InView as="li" onChange={(inView) => inView && fetchMembers()}>
|
||||||
<button type="button" class="light block" onClick={fetchMembers}>
|
<button type="button" class="light block" onClick={fetchMembers}>
|
||||||
Show more…
|
<Trans>Show more…</Trans>
|
||||||
</button>
|
</button>
|
||||||
</InView>
|
</InView>
|
||||||
)}
|
)}
|
||||||
|
@ -299,7 +308,11 @@ function RemoveAddButton({ account, listID }) {
|
||||||
return (
|
return (
|
||||||
<MenuConfirm
|
<MenuConfirm
|
||||||
confirm={!removed}
|
confirm={!removed}
|
||||||
confirmLabel={<span>Remove @{account.username} from list?</span>}
|
confirmLabel={
|
||||||
|
<span>
|
||||||
|
<Trans>Remove @{account.username} from list?</Trans>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
align="end"
|
align="end"
|
||||||
menuItemClassName="danger"
|
menuItemClassName="danger"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
@ -340,7 +353,7 @@ function RemoveAddButton({ account, listID }) {
|
||||||
class={`light ${removed ? '' : 'danger'}`}
|
class={`light ${removed ? '' : 'danger'}`}
|
||||||
disabled={uiState === 'loading'}
|
disabled={uiState === 'loading'}
|
||||||
>
|
>
|
||||||
{removed ? 'Add' : 'Remove…'}
|
{removed ? t`Add` : t`Remove…`}
|
||||||
</button>
|
</button>
|
||||||
</MenuConfirm>
|
</MenuConfirm>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import './lists.css';
|
import './lists.css';
|
||||||
|
|
||||||
import { useEffect, useReducer, useRef, useState } from 'preact/hooks';
|
import { Plural, t, Trans } from '@lingui/macro';
|
||||||
|
import { useEffect, useReducer, useState } from 'preact/hooks';
|
||||||
|
|
||||||
import Icon from '../components/icon';
|
import Icon from '../components/icon';
|
||||||
import Link from '../components/link';
|
import Link from '../components/link';
|
||||||
|
@ -12,7 +13,7 @@ import { fetchLists } from '../utils/lists';
|
||||||
import useTitle from '../utils/useTitle';
|
import useTitle from '../utils/useTitle';
|
||||||
|
|
||||||
function Lists() {
|
function Lists() {
|
||||||
useTitle(`Lists`, `/l`);
|
useTitle(t`Lists`, `/l`);
|
||||||
const [uiState, setUIState] = useState('default');
|
const [uiState, setUIState] = useState('default');
|
||||||
|
|
||||||
const [reloadCount, reload] = useReducer((c) => c + 1, 0);
|
const [reloadCount, reload] = useReducer((c) => c + 1, 0);
|
||||||
|
@ -45,14 +46,16 @@ function Lists() {
|
||||||
<Icon icon="home" size="l" />
|
<Icon icon="home" size="l" />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<h1>Lists</h1>
|
<h1>
|
||||||
|
<Trans>Lists</Trans>
|
||||||
|
</h1>
|
||||||
<div class="header-side">
|
<div class="header-side">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="plain"
|
class="plain"
|
||||||
onClick={() => setShowListAddEditModal(true)}
|
onClick={() => setShowListAddEditModal(true)}
|
||||||
>
|
>
|
||||||
<Icon icon="plus" size="l" alt="New list" />
|
<Icon icon="plus" size="l" alt={t`New list`} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -87,8 +90,7 @@ function Lists() {
|
||||||
{lists.length > 1 && (
|
{lists.length > 1 && (
|
||||||
<footer class="ui-state">
|
<footer class="ui-state">
|
||||||
<small class="insignificant">
|
<small class="insignificant">
|
||||||
{lists.length} list
|
<Plural value={lists.length} one="# list" other="# lists" />
|
||||||
{lists.length === 1 ? '' : 's'}
|
|
||||||
</small>
|
</small>
|
||||||
</footer>
|
</footer>
|
||||||
)}
|
)}
|
||||||
|
@ -98,9 +100,13 @@ function Lists() {
|
||||||
<Loader />
|
<Loader />
|
||||||
</p>
|
</p>
|
||||||
) : uiState === 'error' ? (
|
) : uiState === 'error' ? (
|
||||||
<p class="ui-state">Unable to load lists.</p>
|
<p class="ui-state">
|
||||||
|
<Trans>Unable to load lists.</Trans>
|
||||||
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<p class="ui-state">No lists yet.</p>
|
<p class="ui-state">
|
||||||
|
<Trans>No lists yet.</Trans>
|
||||||
|
</p>
|
||||||
)}
|
)}
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
import './login.css';
|
import './login.css';
|
||||||
|
|
||||||
|
import { t, Trans } from '@lingui/macro';
|
||||||
import Fuse from 'fuse.js';
|
import Fuse from 'fuse.js';
|
||||||
import { useEffect, useRef, useState } from 'preact/hooks';
|
import { useEffect, useRef, useState } from 'preact/hooks';
|
||||||
import { useSearchParams } from 'react-router-dom';
|
import { useSearchParams } from 'react-router-dom';
|
||||||
|
|
||||||
import logo from '../assets/logo.svg';
|
import logo from '../assets/logo.svg';
|
||||||
|
|
||||||
|
import LangSelector from '../components/lang-selector';
|
||||||
import Link from '../components/link';
|
import Link from '../components/link';
|
||||||
import Loader from '../components/loader';
|
import Loader from '../components/loader';
|
||||||
import instancesListURL from '../data/instances.json?url';
|
import instancesListURL from '../data/instances.json?url';
|
||||||
|
@ -137,10 +139,12 @@ function Login() {
|
||||||
<h1>
|
<h1>
|
||||||
<img src={logo} alt="" width="80" height="80" />
|
<img src={logo} alt="" width="80" height="80" />
|
||||||
<br />
|
<br />
|
||||||
Log in
|
<Trans>Log in</Trans>
|
||||||
</h1>
|
</h1>
|
||||||
<label>
|
<label>
|
||||||
<p>Instance</p>
|
<p>
|
||||||
|
<Trans>Instance</Trans>
|
||||||
|
</p>
|
||||||
<input
|
<input
|
||||||
value={instanceText}
|
value={instanceText}
|
||||||
required
|
required
|
||||||
|
@ -154,7 +158,7 @@ function Login() {
|
||||||
autocapitalize="off"
|
autocapitalize="off"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
placeholder="instance domain"
|
placeholder={`instance domain`}
|
||||||
onInput={(e) => {
|
onInput={(e) => {
|
||||||
setInstanceText(e.target.value);
|
setInstanceText(e.target.value);
|
||||||
}}
|
}}
|
||||||
|
@ -177,7 +181,9 @@ function Login() {
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
) : (
|
) : (
|
||||||
<div id="instances-eg">e.g. “mastodon.social”</div>
|
<div id="instances-eg">
|
||||||
|
<Trans>e.g. “mastodon.social”</Trans>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
{/* <datalist id="instances-list">
|
{/* <datalist id="instances-list">
|
||||||
{instancesList.map((instance) => (
|
{instancesList.map((instance) => (
|
||||||
|
@ -187,7 +193,9 @@ function Login() {
|
||||||
</label>
|
</label>
|
||||||
{uiState === 'error' && (
|
{uiState === 'error' && (
|
||||||
<p class="error">
|
<p class="error">
|
||||||
|
<Trans>
|
||||||
Failed to log in. Please try again or another instance.
|
Failed to log in. Please try again or another instance.
|
||||||
|
</Trans>
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
<div>
|
<div>
|
||||||
|
@ -197,8 +205,8 @@ function Login() {
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{selectedInstanceText
|
{selectedInstanceText
|
||||||
? `Continue with ${selectedInstanceText}`
|
? t`Continue with ${selectedInstanceText}`
|
||||||
: 'Continue'}
|
: t`Continue`}
|
||||||
</button>{' '}
|
</button>{' '}
|
||||||
</div>
|
</div>
|
||||||
<Loader hidden={uiState !== 'loading'} />
|
<Loader hidden={uiState !== 'loading'} />
|
||||||
|
@ -206,13 +214,16 @@ function Login() {
|
||||||
{!DEFAULT_INSTANCE && (
|
{!DEFAULT_INSTANCE && (
|
||||||
<p>
|
<p>
|
||||||
<a href="https://joinmastodon.org/servers" target="_blank">
|
<a href="https://joinmastodon.org/servers" target="_blank">
|
||||||
Don't have an account? Create one!
|
<Trans>Don't have an account? Create one!</Trans>
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
<p>
|
<p>
|
||||||
<Link to="/">Go home</Link>
|
<Link to="/">
|
||||||
|
<Trans>Go home</Trans>
|
||||||
|
</Link>
|
||||||
</p>
|
</p>
|
||||||
|
<LangSelector />
|
||||||
</form>
|
</form>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { t, Trans } from '@lingui/macro';
|
||||||
import { useMemo, useRef, useState } from 'preact/hooks';
|
import { useMemo, useRef, useState } from 'preact/hooks';
|
||||||
import { useSearchParams } from 'react-router-dom';
|
import { useSearchParams } from 'react-router-dom';
|
||||||
|
|
||||||
|
@ -16,7 +17,7 @@ function Mentions({ columnMode, ...props }) {
|
||||||
const [searchParams] = columnMode ? [emptySearchParams] : useSearchParams();
|
const [searchParams] = columnMode ? [emptySearchParams] : useSearchParams();
|
||||||
const [stateType, setStateType] = useState(null);
|
const [stateType, setStateType] = useState(null);
|
||||||
const type = props?.type || searchParams.get('type') || stateType;
|
const type = props?.type || searchParams.get('type') || stateType;
|
||||||
useTitle(`Mentions${type === 'private' ? ' (Private)' : ''}`, '/mentions');
|
useTitle(type === 'private' ? t`Private mentions` : t`Mentions`, '/mentions');
|
||||||
|
|
||||||
const mentionsIterator = useRef();
|
const mentionsIterator = useRef();
|
||||||
const latestItem = useRef();
|
const latestItem = useRef();
|
||||||
|
@ -143,7 +144,7 @@ function Mentions({ columnMode, ...props }) {
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
All
|
<Trans>All</Trans>
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
to="/mentions?type=private"
|
to="/mentions?type=private"
|
||||||
|
@ -155,7 +156,7 @@ function Mentions({ columnMode, ...props }) {
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Private
|
<Trans>Private</Trans>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -163,10 +164,10 @@ function Mentions({ columnMode, ...props }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Timeline
|
<Timeline
|
||||||
title="Mentions"
|
title={t`Mentions`}
|
||||||
id="mentions"
|
id="mentions"
|
||||||
emptyText="No one mentioned you :("
|
emptyText={t`No one mentioned you :(`}
|
||||||
errorText="Unable to load mentions."
|
errorText={t`Unable to load mentions.`}
|
||||||
instance={instance}
|
instance={instance}
|
||||||
fetchItems={fetchItems}
|
fetchItems={fetchItems}
|
||||||
checkForUpdates={checkForUpdates}
|
checkForUpdates={checkForUpdates}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import './notifications.css';
|
import './notifications.css';
|
||||||
|
|
||||||
|
import { Plural, t, Trans } from '@lingui/macro';
|
||||||
import { Fragment } from 'preact';
|
import { Fragment } from 'preact';
|
||||||
import { memo } from 'preact/compat';
|
import { memo } from 'preact/compat';
|
||||||
import { useCallback, useEffect, useRef, useState } from 'preact/hooks';
|
import { useCallback, useEffect, useRef, useState } from 'preact/hooks';
|
||||||
|
@ -85,7 +86,7 @@ export function getGroupedNotifications(notifications) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function Notifications({ columnMode }) {
|
function Notifications({ columnMode }) {
|
||||||
useTitle('Notifications', '/notifications');
|
useTitle(t`Notifications`, '/notifications');
|
||||||
const { masto, instance } = api();
|
const { masto, instance } = api();
|
||||||
const snapStates = useSnapshot(states);
|
const snapStates = useSnapshot(states);
|
||||||
const [uiState, setUIState] = useState('default');
|
const [uiState, setUIState] = useState('default');
|
||||||
|
@ -484,10 +485,12 @@ function Notifications({ columnMode }) {
|
||||||
<div class="header-side">
|
<div class="header-side">
|
||||||
<NavMenu />
|
<NavMenu />
|
||||||
<Link to="/" class="button plain">
|
<Link to="/" class="button plain">
|
||||||
<Icon icon="home" size="l" alt="Home" />
|
<Icon icon="home" size="l" alt={t`Home`} />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<h1>Notifications</h1>
|
<h1>
|
||||||
|
<Trans>Notifications</Trans>
|
||||||
|
</h1>
|
||||||
<div class="header-side">
|
<div class="header-side">
|
||||||
{supportsFilteredNotifications && (
|
{supportsFilteredNotifications && (
|
||||||
<button
|
<button
|
||||||
|
@ -497,7 +500,11 @@ function Notifications({ columnMode }) {
|
||||||
setShowNotificationsSettings(true);
|
setShowNotificationsSettings(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon icon="settings" size="l" alt="Notifications settings" />
|
<Icon
|
||||||
|
icon="settings"
|
||||||
|
size="l"
|
||||||
|
alt={t`Notifications settings`}
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -514,7 +521,7 @@ function Notifications({ columnMode }) {
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon icon="arrow-up" /> New notifications
|
<Icon icon="arrow-up" /> <Trans>New notifications</Trans>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</header>
|
</header>
|
||||||
|
@ -525,7 +532,11 @@ function Notifications({ columnMode }) {
|
||||||
<summary>
|
<summary>
|
||||||
<span>
|
<span>
|
||||||
<Icon icon="announce" class="announcement-icon" size="l" />{' '}
|
<Icon icon="announce" class="announcement-icon" size="l" />{' '}
|
||||||
<b>Announcement{announcements.length > 1 ? 's' : ''}</b>{' '}
|
<Plural
|
||||||
|
value={announcements.length}
|
||||||
|
one="Announcement"
|
||||||
|
other="Announcements"
|
||||||
|
/>{' '}
|
||||||
<small class="insignificant">{instance}</small>
|
<small class="insignificant">{instance}</small>
|
||||||
</span>
|
</span>
|
||||||
{announcements.length > 1 && (
|
{announcements.length > 1 && (
|
||||||
|
@ -567,10 +578,18 @@ function Notifications({ columnMode }) {
|
||||||
)}
|
)}
|
||||||
{followRequests.length > 0 && (
|
{followRequests.length > 0 && (
|
||||||
<div class="follow-requests">
|
<div class="follow-requests">
|
||||||
<h2 class="timeline-header">Follow requests</h2>
|
<h2 class="timeline-header">
|
||||||
|
<Trans>Follow requests</Trans>
|
||||||
|
</h2>
|
||||||
{followRequests.length > 5 ? (
|
{followRequests.length > 5 ? (
|
||||||
<details>
|
<details>
|
||||||
<summary>{followRequests.length} follow requests</summary>
|
<summary>
|
||||||
|
<Plural
|
||||||
|
value={followRequests.length}
|
||||||
|
one="# follow request"
|
||||||
|
other="# follow requests"
|
||||||
|
/>
|
||||||
|
</summary>
|
||||||
<ul>
|
<ul>
|
||||||
{followRequests.map((account) => (
|
{followRequests.map((account) => (
|
||||||
<li key={account.id}>
|
<li key={account.id}>
|
||||||
|
@ -620,8 +639,11 @@ function Notifications({ columnMode }) {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<summary>
|
<summary>
|
||||||
Filtered notifications from{' '}
|
<Plural
|
||||||
{notificationsPolicy.summary.pendingRequestsCount} people
|
value={notificationsPolicy.summary.pendingRequestsCount}
|
||||||
|
one="Filtered notifications from # person"
|
||||||
|
other="Filtered notifications from # people"
|
||||||
|
/>
|
||||||
</summary>
|
</summary>
|
||||||
{!notificationsRequests ? (
|
{!notificationsRequests ? (
|
||||||
<p class="ui-state">
|
<p class="ui-state">
|
||||||
|
@ -683,13 +705,15 @@ function Notifications({ columnMode }) {
|
||||||
setOnlyMentions(e.target.checked);
|
setOnlyMentions(e.target.checked);
|
||||||
}}
|
}}
|
||||||
/>{' '}
|
/>{' '}
|
||||||
Only mentions
|
<Trans>Only mentions</Trans>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<h2 class="timeline-header">Today</h2>
|
<h2 class="timeline-header">
|
||||||
|
<Trans>Today</Trans>
|
||||||
|
</h2>
|
||||||
{showTodayEmpty && (
|
{showTodayEmpty && (
|
||||||
<p class="ui-state insignificant">
|
<p class="ui-state insignificant">
|
||||||
{uiState === 'default' ? "You're all caught up." : <>…</>}
|
{uiState === 'default' ? t`You're all caught up.` : <>…</>}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
{snapStates.notifications.length ? (
|
{snapStates.notifications.length ? (
|
||||||
|
@ -712,7 +736,7 @@ function Notifications({ columnMode }) {
|
||||||
const heading =
|
const heading =
|
||||||
notificationDay.toDateString() ===
|
notificationDay.toDateString() ===
|
||||||
yesterdayDate.toDateString()
|
yesterdayDate.toDateString()
|
||||||
? 'Yesterday'
|
? t`Yesterday`
|
||||||
: niceDateTime(currentDay, {
|
: niceDateTime(currentDay, {
|
||||||
hideTime: true,
|
hideTime: true,
|
||||||
});
|
});
|
||||||
|
@ -748,11 +772,11 @@ function Notifications({ columnMode }) {
|
||||||
)}
|
)}
|
||||||
{uiState === 'error' && (
|
{uiState === 'error' && (
|
||||||
<p class="ui-state">
|
<p class="ui-state">
|
||||||
Unable to load notifications
|
<Trans>Unable to load notifications</Trans>
|
||||||
<br />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
<button type="button" onClick={() => loadNotifications(true)}>
|
<button type="button" onClick={() => loadNotifications(true)}>
|
||||||
Try again
|
<Trans>Try again</Trans>
|
||||||
</button>
|
</button>
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
@ -776,7 +800,7 @@ function Notifications({ columnMode }) {
|
||||||
{uiState === 'loading' ? (
|
{uiState === 'loading' ? (
|
||||||
<Loader abrupt />
|
<Loader abrupt />
|
||||||
) : (
|
) : (
|
||||||
<>Show more…</>
|
<Trans>Show more…</Trans>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
</InView>
|
</InView>
|
||||||
|
@ -796,10 +820,12 @@ function Notifications({ columnMode }) {
|
||||||
class="sheet-close"
|
class="sheet-close"
|
||||||
onClick={() => setShowNotificationsSettings(false)}
|
onClick={() => setShowNotificationsSettings(false)}
|
||||||
>
|
>
|
||||||
<Icon icon="x" />
|
<Icon icon="x" alt={t`Close`} />
|
||||||
</button>
|
</button>
|
||||||
<header>
|
<header>
|
||||||
<h2>Notifications settings</h2>
|
<h2>
|
||||||
|
<Trans>Notifications settings</Trans>
|
||||||
|
</h2>
|
||||||
</header>
|
</header>
|
||||||
<main>
|
<main>
|
||||||
<form
|
<form
|
||||||
|
@ -825,14 +851,16 @@ function Notifications({ columnMode }) {
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
await masto.v1.notifications.policy.update(allFilters);
|
await masto.v1.notifications.policy.update(allFilters);
|
||||||
showToast('Notifications settings updated');
|
showToast(t`Notifications settings updated`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<p>Filter out notifications from people:</p>
|
<p>
|
||||||
|
<Trans>Filter out notifications from people:</Trans>
|
||||||
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<label>
|
<label>
|
||||||
<input
|
<input
|
||||||
|
@ -841,7 +869,7 @@ function Notifications({ columnMode }) {
|
||||||
defaultChecked={notificationsPolicy.filterNotFollowing}
|
defaultChecked={notificationsPolicy.filterNotFollowing}
|
||||||
name="filterNotFollowing"
|
name="filterNotFollowing"
|
||||||
/>{' '}
|
/>{' '}
|
||||||
You don't follow
|
<Trans>You don't follow</Trans>
|
||||||
</label>
|
</label>
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
|
@ -852,7 +880,7 @@ function Notifications({ columnMode }) {
|
||||||
defaultChecked={notificationsPolicy.filterNotFollowers}
|
defaultChecked={notificationsPolicy.filterNotFollowers}
|
||||||
name="filterNotFollowers"
|
name="filterNotFollowers"
|
||||||
/>{' '}
|
/>{' '}
|
||||||
Who don't follow you
|
<Trans>Who don't follow you</Trans>
|
||||||
</label>
|
</label>
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
|
@ -863,7 +891,7 @@ function Notifications({ columnMode }) {
|
||||||
defaultChecked={notificationsPolicy.filterNewAccounts}
|
defaultChecked={notificationsPolicy.filterNewAccounts}
|
||||||
name="filterNewAccounts"
|
name="filterNewAccounts"
|
||||||
/>{' '}
|
/>{' '}
|
||||||
With a new account
|
<Trans>With a new account</Trans>
|
||||||
</label>
|
</label>
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
|
@ -874,11 +902,13 @@ function Notifications({ columnMode }) {
|
||||||
defaultChecked={notificationsPolicy.filterPrivateMentions}
|
defaultChecked={notificationsPolicy.filterPrivateMentions}
|
||||||
name="filterPrivateMentions"
|
name="filterPrivateMentions"
|
||||||
/>{' '}
|
/>{' '}
|
||||||
Who unsolicitedly private mention you
|
<Trans>Who unsolicitedly private mention you</Trans>
|
||||||
</label>
|
</label>
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<button type="submit">Save</button>
|
<button type="submit">
|
||||||
|
<Trans>Save</Trans>
|
||||||
|
</button>
|
||||||
</p>
|
</p>
|
||||||
</form>
|
</form>
|
||||||
</main>
|
</main>
|
||||||
|
@ -940,10 +970,12 @@ function AnnouncementBlock({ announcement }) {
|
||||||
{' '}
|
{' '}
|
||||||
•{' '}
|
•{' '}
|
||||||
<span class="ib">
|
<span class="ib">
|
||||||
|
<Trans>
|
||||||
Updated{' '}
|
Updated{' '}
|
||||||
<time datetime={updatedAtDate.toISOString()}>
|
<time datetime={updatedAtDate.toISOString()}>
|
||||||
{niceDateTime(updatedAtDate)}
|
{niceDateTime(updatedAtDate)}
|
||||||
</time>
|
</time>
|
||||||
|
</Trans>
|
||||||
</span>
|
</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
@ -1005,7 +1037,9 @@ function NotificationRequestModalButton({ request }) {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon icon="notification" class="more-insignificant" />{' '}
|
<Icon icon="notification" class="more-insignificant" />{' '}
|
||||||
<small>View notifications from @{account.username}</small>{' '}
|
<small>
|
||||||
|
<Trans>View notifications from @{account.username}</Trans>
|
||||||
|
</small>{' '}
|
||||||
<Icon icon="chevron-down" />
|
<Icon icon="chevron-down" />
|
||||||
</button>
|
</button>
|
||||||
{showModal && (
|
{showModal && (
|
||||||
|
@ -1018,10 +1052,12 @@ function NotificationRequestModalButton({ request }) {
|
||||||
>
|
>
|
||||||
<div class="sheet" tabIndex="-1">
|
<div class="sheet" tabIndex="-1">
|
||||||
<button type="button" class="sheet-close" onClick={onClose}>
|
<button type="button" class="sheet-close" onClick={onClose}>
|
||||||
<Icon icon="x" />
|
<Icon icon="x" alt={t`Close`} />
|
||||||
</button>
|
</button>
|
||||||
<header>
|
<header>
|
||||||
<b>Notifications from @{account.username}</b>
|
<b>
|
||||||
|
<Trans>Notifications from @{account.username}</Trans>
|
||||||
|
</b>
|
||||||
</header>
|
</header>
|
||||||
<main>
|
<main>
|
||||||
{uiState === 'loading' ? (
|
{uiState === 'loading' ? (
|
||||||
|
@ -1084,17 +1120,17 @@ function NotificationRequestButtons({ request, onChange }) {
|
||||||
state: 'accept',
|
state: 'accept',
|
||||||
});
|
});
|
||||||
showToast(
|
showToast(
|
||||||
`Notifications from @${request.account.username} will not be filtered from now on.`,
|
t`Notifications from @${request.account.username} will not be filtered from now on.`,
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setUIState('error');
|
setUIState('error');
|
||||||
console.error(error);
|
console.error(error);
|
||||||
showToast(`Unable to accept notification request`);
|
showToast(t`Unable to accept notification request`);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Allow
|
<Trans>Allow</Trans>
|
||||||
</button>{' '}
|
</button>{' '}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
@ -1114,17 +1150,17 @@ function NotificationRequestButtons({ request, onChange }) {
|
||||||
state: 'dismiss',
|
state: 'dismiss',
|
||||||
});
|
});
|
||||||
showToast(
|
showToast(
|
||||||
`Notifications from @${request.account.username} will not show up in Filtered notifications from now on.`,
|
t`Notifications from @${request.account.username} will not show up in Filtered notifications from now on.`,
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setUIState('error');
|
setUIState('error');
|
||||||
console.error(error);
|
console.error(error);
|
||||||
showToast(`Unable to dismiss notification request`);
|
showToast(t`Unable to dismiss notification request`);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Dismiss
|
<Trans>Dismiss</Trans>
|
||||||
</button>
|
</button>
|
||||||
<span class="notification-request-states">
|
<span class="notification-request-states">
|
||||||
{uiState === 'loading' ? (
|
{uiState === 'loading' ? (
|
||||||
|
@ -1132,14 +1168,14 @@ function NotificationRequestButtons({ request, onChange }) {
|
||||||
) : requestState === 'accept' ? (
|
) : requestState === 'accept' ? (
|
||||||
<Icon
|
<Icon
|
||||||
icon="check-circle"
|
icon="check-circle"
|
||||||
alt="Accepted"
|
alt={t`Accepted`}
|
||||||
class="notification-accepted"
|
class="notification-accepted"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
requestState === 'dismiss' && (
|
requestState === 'dismiss' && (
|
||||||
<Icon
|
<Icon
|
||||||
icon="x-circle"
|
icon="x-circle"
|
||||||
alt="Dismissed"
|
alt={t`Dismissed`}
|
||||||
class="notification-dismissed"
|
class="notification-dismissed"
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { t, Trans } from '@lingui/macro';
|
||||||
import { Menu, MenuDivider, MenuItem } from '@szhsin/react-menu';
|
import { Menu, MenuDivider, MenuItem } from '@szhsin/react-menu';
|
||||||
import { useRef } from 'preact/hooks';
|
import { useRef } from 'preact/hooks';
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
|
@ -22,7 +23,9 @@ function Public({ local, columnMode, ...props }) {
|
||||||
instance: props?.instance || params.instance,
|
instance: props?.instance || params.instance,
|
||||||
});
|
});
|
||||||
const { masto: currentMasto, instance: currentInstance } = api();
|
const { masto: currentMasto, instance: currentInstance } = api();
|
||||||
const title = `${isLocal ? 'Local' : 'Federated'} timeline (${instance})`;
|
const title = isLocal
|
||||||
|
? t`Local timeline (${instance})`
|
||||||
|
: t`Federated timeline (${instance})`;
|
||||||
useTitle(title, isLocal ? `/:instance?/p/l` : `/:instance?/p`);
|
useTitle(title, isLocal ? `/:instance?/p/l` : `/:instance?/p`);
|
||||||
// const navigate = useNavigate();
|
// const navigate = useNavigate();
|
||||||
const latestItem = useRef();
|
const latestItem = useRef();
|
||||||
|
@ -84,14 +87,14 @@ function Public({ local, columnMode, ...props }) {
|
||||||
title={title}
|
title={title}
|
||||||
titleComponent={
|
titleComponent={
|
||||||
<h1 class="header-double-lines">
|
<h1 class="header-double-lines">
|
||||||
<b>{isLocal ? 'Local timeline' : 'Federated timeline'}</b>
|
<b>{isLocal ? t`Local timeline` : t`Federated timeline`}</b>
|
||||||
<div>{instance}</div>
|
<div>{instance}</div>
|
||||||
</h1>
|
</h1>
|
||||||
}
|
}
|
||||||
id="public"
|
id="public"
|
||||||
instance={instance}
|
instance={instance}
|
||||||
emptyText="No one has posted anything yet."
|
emptyText={t`No one has posted anything yet.`}
|
||||||
errorText="Unable to load posts"
|
errorText={t`Unable to load posts`}
|
||||||
fetchItems={fetchPublic}
|
fetchItems={fetchPublic}
|
||||||
checkForUpdates={checkForUpdates}
|
checkForUpdates={checkForUpdates}
|
||||||
useItemID
|
useItemID
|
||||||
|
@ -108,18 +111,24 @@ function Public({ local, columnMode, ...props }) {
|
||||||
position="anchor"
|
position="anchor"
|
||||||
menuButton={
|
menuButton={
|
||||||
<button type="button" class="plain">
|
<button type="button" class="plain">
|
||||||
<Icon icon="more" size="l" />
|
<Icon icon="more" size="l" alt={t`More`} />
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<MenuItem href={isLocal ? `/#/${instance}/p` : `/#/${instance}/p/l`}>
|
<MenuItem href={isLocal ? `/#/${instance}/p` : `/#/${instance}/p/l`}>
|
||||||
{isLocal ? (
|
{isLocal ? (
|
||||||
<>
|
<>
|
||||||
<Icon icon="transfer" /> <span>Switch to Federated</span>
|
<Icon icon="transfer" />{' '}
|
||||||
|
<span>
|
||||||
|
<Trans>Switch to Federated</Trans>
|
||||||
|
</span>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Icon icon="transfer" /> <span>Switch to Local</span>
|
<Icon icon="transfer" />{' '}
|
||||||
|
<span>
|
||||||
|
<Trans>Switch to Local</Trans>
|
||||||
|
</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
@ -127,10 +136,10 @@ function Public({ local, columnMode, ...props }) {
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
let newInstance = prompt(
|
let newInstance = prompt(
|
||||||
'Enter a new instance e.g. "mastodon.social"',
|
t`Enter a new instance e.g. "mastodon.social"`,
|
||||||
);
|
);
|
||||||
if (!/\./.test(newInstance)) {
|
if (!/\./.test(newInstance)) {
|
||||||
if (newInstance) alert('Invalid instance');
|
if (newInstance) alert(t`Invalid instance`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (newInstance) {
|
if (newInstance) {
|
||||||
|
@ -142,7 +151,10 @@ function Public({ local, columnMode, ...props }) {
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon icon="bus" /> <span>Go to another instance…</span>
|
<Icon icon="bus" />{' '}
|
||||||
|
<span>
|
||||||
|
<Trans>Go to another instance…</Trans>
|
||||||
|
</span>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
{currentInstance !== instance && (
|
{currentInstance !== instance && (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
|
@ -154,7 +166,9 @@ function Public({ local, columnMode, ...props }) {
|
||||||
>
|
>
|
||||||
<Icon icon="bus" />{' '}
|
<Icon icon="bus" />{' '}
|
||||||
<small class="menu-double-lines">
|
<small class="menu-double-lines">
|
||||||
|
<Trans>
|
||||||
Go to my instance (<b>{currentInstance}</b>)
|
Go to my instance (<b>{currentInstance}</b>)
|
||||||
|
</Trans>
|
||||||
</small>
|
</small>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import './search.css';
|
import './search.css';
|
||||||
|
|
||||||
import { useAutoAnimate } from '@formkit/auto-animate/preact';
|
import { useAutoAnimate } from '@formkit/auto-animate/preact';
|
||||||
|
import { t, Trans } from '@lingui/macro';
|
||||||
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';
|
||||||
import { InView } from 'react-intersection-observer';
|
import { InView } from 'react-intersection-observer';
|
||||||
|
@ -35,22 +36,23 @@ function Search({ columnMode, ...props }) {
|
||||||
const type = columnMode
|
const type = columnMode
|
||||||
? 'statuses'
|
? 'statuses'
|
||||||
: props?.type || searchParams.get('type');
|
: props?.type || searchParams.get('type');
|
||||||
useTitle(
|
let title = t`Search`;
|
||||||
q
|
if (q) {
|
||||||
? `Search: ${q}${
|
switch (type) {
|
||||||
type
|
case 'statuses':
|
||||||
? ` (${
|
title = t`Search: ${q} (Posts)`;
|
||||||
{
|
break;
|
||||||
statuses: 'Posts',
|
case 'accounts':
|
||||||
accounts: 'Accounts',
|
title = t`Search: ${q} (Accounts)`;
|
||||||
hashtags: 'Hashtags',
|
break;
|
||||||
}[type]
|
case 'hashtags':
|
||||||
})`
|
title = t`Search: ${q} (Hashtags)`;
|
||||||
: ''
|
break;
|
||||||
}`
|
default:
|
||||||
: 'Search',
|
title = t`Search: ${q}`;
|
||||||
`/search`,
|
}
|
||||||
);
|
}
|
||||||
|
useTitle(title, `/search`);
|
||||||
|
|
||||||
const [showMore, setShowMore] = useState(false);
|
const [showMore, setShowMore] = useState(false);
|
||||||
const offsetRef = useRef(0);
|
const offsetRef = useRef(0);
|
||||||
|
@ -204,7 +206,7 @@ function Search({ columnMode, ...props }) {
|
||||||
}}
|
}}
|
||||||
disabled={uiState === 'loading'}
|
disabled={uiState === 'loading'}
|
||||||
>
|
>
|
||||||
<Icon icon="search" size="l" />
|
<Icon icon="search" size="l" alt={t`Search`} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -217,22 +219,22 @@ function Search({ columnMode, ...props }) {
|
||||||
>
|
>
|
||||||
{!!type && (
|
{!!type && (
|
||||||
<Link to={`/search${q ? `?q=${encodeURIComponent(q)}` : ''}`}>
|
<Link to={`/search${q ? `?q=${encodeURIComponent(q)}` : ''}`}>
|
||||||
‹ All
|
<Icon icon="chevron-left" /> <Trans>All</Trans>
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
{[
|
{[
|
||||||
{
|
{
|
||||||
label: 'Accounts',
|
label: t`Accounts`,
|
||||||
type: 'accounts',
|
type: 'accounts',
|
||||||
to: `/search?q=${encodeURIComponent(q)}&type=accounts`,
|
to: `/search?q=${encodeURIComponent(q)}&type=accounts`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Hashtags',
|
label: t`Hashtags`,
|
||||||
type: 'hashtags',
|
type: 'hashtags',
|
||||||
to: `/search?q=${encodeURIComponent(q)}&type=hashtags`,
|
to: `/search?q=${encodeURIComponent(q)}&type=hashtags`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Posts',
|
label: t`Posts`,
|
||||||
type: 'statuses',
|
type: 'statuses',
|
||||||
to: `/search?q=${encodeURIComponent(q)}&type=statuses`,
|
to: `/search?q=${encodeURIComponent(q)}&type=statuses`,
|
||||||
},
|
},
|
||||||
|
@ -255,11 +257,11 @@ function Search({ columnMode, ...props }) {
|
||||||
<>
|
<>
|
||||||
{type !== 'accounts' && (
|
{type !== 'accounts' && (
|
||||||
<h2 class="timeline-header">
|
<h2 class="timeline-header">
|
||||||
Accounts{' '}
|
<Trans>Accounts</Trans>{' '}
|
||||||
<Link
|
<Link
|
||||||
to={`/search?q=${encodeURIComponent(q)}&type=accounts`}
|
to={`/search?q=${encodeURIComponent(q)}&type=accounts`}
|
||||||
>
|
>
|
||||||
<Icon icon="arrow-right" size="l" />
|
<Icon icon="arrow-right" size="l" alt={t`See more`} />
|
||||||
</Link>
|
</Link>
|
||||||
</h2>
|
</h2>
|
||||||
)}
|
)}
|
||||||
|
@ -285,7 +287,8 @@ function Search({ columnMode, ...props }) {
|
||||||
q,
|
q,
|
||||||
)}&type=accounts`}
|
)}&type=accounts`}
|
||||||
>
|
>
|
||||||
See more accounts <Icon icon="arrow-right" />
|
<Trans>See more accounts</Trans>{' '}
|
||||||
|
<Icon icon="arrow-right" />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@ -297,7 +300,9 @@ function Search({ columnMode, ...props }) {
|
||||||
<Loader abrupt />
|
<Loader abrupt />
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<p class="ui-state">No accounts found.</p>
|
<p class="ui-state">
|
||||||
|
<Trans>No accounts found.</Trans>
|
||||||
|
</p>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
@ -306,11 +311,11 @@ function Search({ columnMode, ...props }) {
|
||||||
<>
|
<>
|
||||||
{type !== 'hashtags' && (
|
{type !== 'hashtags' && (
|
||||||
<h2 class="timeline-header">
|
<h2 class="timeline-header">
|
||||||
Hashtags{' '}
|
<Trans>Hashtags</Trans>{' '}
|
||||||
<Link
|
<Link
|
||||||
to={`/search?q=${encodeURIComponent(q)}&type=hashtags`}
|
to={`/search?q=${encodeURIComponent(q)}&type=hashtags`}
|
||||||
>
|
>
|
||||||
<Icon icon="arrow-right" size="l" />
|
<Icon icon="arrow-right" size="l" alt={t`See more`} />
|
||||||
</Link>
|
</Link>
|
||||||
</h2>
|
</h2>
|
||||||
)}
|
)}
|
||||||
|
@ -332,7 +337,7 @@ function Search({ columnMode, ...props }) {
|
||||||
: `/t/${name}`
|
: `/t/${name}`
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Icon icon="hashtag" />
|
<Icon icon="hashtag" alt="#" />
|
||||||
<span>{name}</span>
|
<span>{name}</span>
|
||||||
{!!total && (
|
{!!total && (
|
||||||
<span class="count">
|
<span class="count">
|
||||||
|
@ -352,7 +357,8 @@ function Search({ columnMode, ...props }) {
|
||||||
q,
|
q,
|
||||||
)}&type=hashtags`}
|
)}&type=hashtags`}
|
||||||
>
|
>
|
||||||
See more hashtags <Icon icon="arrow-right" />
|
<Trans>See more hashtags</Trans>{' '}
|
||||||
|
<Icon icon="arrow-right" />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@ -364,7 +370,9 @@ function Search({ columnMode, ...props }) {
|
||||||
<Loader abrupt />
|
<Loader abrupt />
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<p class="ui-state">No hashtags found.</p>
|
<p class="ui-state">
|
||||||
|
<Trans>No hashtags found.</Trans>
|
||||||
|
</p>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
@ -373,11 +381,11 @@ function Search({ columnMode, ...props }) {
|
||||||
<>
|
<>
|
||||||
{type !== 'statuses' && (
|
{type !== 'statuses' && (
|
||||||
<h2 class="timeline-header">
|
<h2 class="timeline-header">
|
||||||
Posts{' '}
|
<Trans>Posts</Trans>{' '}
|
||||||
<Link
|
<Link
|
||||||
to={`/search?q=${encodeURIComponent(q)}&type=statuses`}
|
to={`/search?q=${encodeURIComponent(q)}&type=statuses`}
|
||||||
>
|
>
|
||||||
<Icon icon="arrow-right" size="l" />
|
<Icon icon="arrow-right" size="l" alt={t`See more`} />
|
||||||
</Link>
|
</Link>
|
||||||
</h2>
|
</h2>
|
||||||
)}
|
)}
|
||||||
|
@ -407,7 +415,8 @@ function Search({ columnMode, ...props }) {
|
||||||
q,
|
q,
|
||||||
)}&type=statuses`}
|
)}&type=statuses`}
|
||||||
>
|
>
|
||||||
See more posts <Icon icon="arrow-right" />
|
<Trans>See more posts</Trans>{' '}
|
||||||
|
<Icon icon="arrow-right" />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@ -419,7 +428,9 @@ function Search({ columnMode, ...props }) {
|
||||||
<Loader abrupt />
|
<Loader abrupt />
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<p class="ui-state">No posts found.</p>
|
<p class="ui-state">
|
||||||
|
<Trans>No posts found.</Trans>
|
||||||
|
</p>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
@ -440,11 +451,13 @@ function Search({ columnMode, ...props }) {
|
||||||
onClick={() => loadResults()}
|
onClick={() => loadResults()}
|
||||||
style={{ marginBlockEnd: '6em' }}
|
style={{ marginBlockEnd: '6em' }}
|
||||||
>
|
>
|
||||||
Show more…
|
<Trans>Show more…</Trans>
|
||||||
</button>
|
</button>
|
||||||
</InView>
|
</InView>
|
||||||
) : (
|
) : (
|
||||||
<p class="ui-state insignificant">The end.</p>
|
<p class="ui-state insignificant">
|
||||||
|
<Trans>The end.</Trans>
|
||||||
|
</p>
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
uiState === 'loading' && (
|
uiState === 'loading' && (
|
||||||
|
@ -460,7 +473,9 @@ function Search({ columnMode, ...props }) {
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<p class="ui-state">
|
<p class="ui-state">
|
||||||
|
<Trans>
|
||||||
Enter your search term or paste a URL above to get started.
|
Enter your search term or paste a URL above to get started.
|
||||||
|
</Trans>
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</main>
|
</main>
|
||||||
|
|
|
@ -143,14 +143,14 @@
|
||||||
background-color: var(--bg-faded-color);
|
background-color: var(--bg-faded-color);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
margin: 8px 0;
|
margin: 8px 0;
|
||||||
max-height: 6.5em;
|
max-height: 10em;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
font-size: 90%;
|
font-size: 90%;
|
||||||
}
|
}
|
||||||
#settings-container .checkbox-fieldset label {
|
#settings-container .checkbox-fieldset label {
|
||||||
flex: 1 0 10em;
|
flex: 1 0 12em;
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
import './settings.css';
|
import './settings.css';
|
||||||
|
|
||||||
|
import { Plural, t, Trans } from '@lingui/macro';
|
||||||
import { useEffect, useRef, useState } from 'preact/hooks';
|
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 LangSelector from '../components/lang-selector';
|
||||||
import Link from '../components/link';
|
import Link from '../components/link';
|
||||||
import RelativeTime from '../components/relative-time';
|
import RelativeTime from '../components/relative-time';
|
||||||
import targetLanguages from '../data/lingva-target-languages';
|
import targetLanguages from '../data/lingva-target-languages';
|
||||||
|
@ -64,18 +66,22 @@ function Settings({ onClose }) {
|
||||||
<div id="settings-container" class="sheet" tabIndex="-1">
|
<div id="settings-container" class="sheet" tabIndex="-1">
|
||||||
{!!onClose && (
|
{!!onClose && (
|
||||||
<button type="button" class="sheet-close" onClick={onClose}>
|
<button type="button" class="sheet-close" onClick={onClose}>
|
||||||
<Icon icon="x" />
|
<Icon icon="x" alt={t`Close`} />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<header>
|
<header>
|
||||||
<h2>Settings</h2>
|
<h2>
|
||||||
|
<Trans>Settings</Trans>
|
||||||
|
</h2>
|
||||||
</header>
|
</header>
|
||||||
<main>
|
<main>
|
||||||
<section>
|
<section>
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
<div>
|
<div>
|
||||||
<label>Appearance</label>
|
<label>
|
||||||
|
<Trans>Appearance</Trans>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<form
|
<form
|
||||||
|
@ -149,7 +155,9 @@ function Settings({ onClose }) {
|
||||||
value="light"
|
value="light"
|
||||||
defaultChecked={currentTheme === 'light'}
|
defaultChecked={currentTheme === 'light'}
|
||||||
/>
|
/>
|
||||||
<span>Light</span>
|
<span>
|
||||||
|
<Trans>Light</Trans>
|
||||||
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
<input
|
<input
|
||||||
|
@ -158,7 +166,9 @@ function Settings({ onClose }) {
|
||||||
value="dark"
|
value="dark"
|
||||||
defaultChecked={currentTheme === 'dark'}
|
defaultChecked={currentTheme === 'dark'}
|
||||||
/>
|
/>
|
||||||
<span>Dark</span>
|
<span>
|
||||||
|
<Trans>Dark</Trans>
|
||||||
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
<input
|
<input
|
||||||
|
@ -169,7 +179,9 @@ function Settings({ onClose }) {
|
||||||
currentTheme !== 'light' && currentTheme !== 'dark'
|
currentTheme !== 'light' && currentTheme !== 'dark'
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<span>Auto</span>
|
<span>
|
||||||
|
<Trans>Auto</Trans>
|
||||||
|
</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
@ -177,10 +189,16 @@ function Settings({ onClose }) {
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<div>
|
<div>
|
||||||
<label>Text size</label>
|
<label>
|
||||||
|
<Trans>Text size</Trans>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="range-group">
|
<div class="range-group">
|
||||||
<span style={{ fontSize: TEXT_SIZES[0] }}>A</span>{' '}
|
<span style={{ fontSize: TEXT_SIZES[0] }}>
|
||||||
|
<Trans comment="Preview of one character, in smallest size">
|
||||||
|
A
|
||||||
|
</Trans>
|
||||||
|
</span>{' '}
|
||||||
<input
|
<input
|
||||||
type="range"
|
type="range"
|
||||||
min={TEXT_SIZES[0]}
|
min={TEXT_SIZES[0]}
|
||||||
|
@ -202,7 +220,9 @@ function Settings({ onClose }) {
|
||||||
}}
|
}}
|
||||||
/>{' '}
|
/>{' '}
|
||||||
<span style={{ fontSize: TEXT_SIZES[TEXT_SIZES.length - 1] }}>
|
<span style={{ fontSize: TEXT_SIZES[TEXT_SIZES.length - 1] }}>
|
||||||
|
<Trans comment="Preview of one character, in largest size">
|
||||||
A
|
A
|
||||||
|
</Trans>
|
||||||
</span>
|
</span>
|
||||||
<datalist id="sizes">
|
<datalist id="sizes">
|
||||||
{TEXT_SIZES.map((size) => (
|
{TEXT_SIZES.map((size) => (
|
||||||
|
@ -211,18 +231,26 @@ function Settings({ onClose }) {
|
||||||
</datalist>
|
</datalist>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<label>
|
||||||
|
<Trans>Display language</Trans>
|
||||||
|
</label>
|
||||||
|
<LangSelector />
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
{authenticated && (
|
{authenticated && (
|
||||||
<>
|
<>
|
||||||
<h3>Posting</h3>
|
<h3>
|
||||||
|
<Trans>Posting</Trans>
|
||||||
|
</h3>
|
||||||
<section>
|
<section>
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
<div>
|
<div>
|
||||||
<label for="posting-privacy-field">
|
<label for="posting-privacy-field">
|
||||||
Default visibility{' '}
|
<Trans>Default visibility</Trans>{' '}
|
||||||
<Icon icon="cloud" alt="Synced" class="synced-icon" />
|
<Icon icon="cloud" alt={t`Synced`} class="synced-icon" />
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
@ -247,23 +275,30 @@ function Settings({ onClose }) {
|
||||||
'posting:default:visibility': value,
|
'posting:default:visibility': value,
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
alert('Failed to update posting privacy');
|
alert(t`Failed to update posting privacy`);
|
||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<option value="public">Public</option>
|
<option value="public">
|
||||||
<option value="unlisted">Unlisted</option>
|
<Trans>Public</Trans>
|
||||||
<option value="private">Followers only</option>
|
</option>
|
||||||
|
<option value="unlisted">
|
||||||
|
<Trans>Unlisted</Trans>
|
||||||
|
</option>
|
||||||
|
<option value="private">
|
||||||
|
<Trans>Followers only</Trans>
|
||||||
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
<p class="section-postnote">
|
<p class="section-postnote">
|
||||||
<Icon icon="cloud" alt="Synced" class="synced-icon" />{' '}
|
<Icon icon="cloud" alt={t`Synced`} class="synced-icon" />{' '}
|
||||||
<small>
|
<small>
|
||||||
|
<Trans>
|
||||||
Synced to your instance server's settings.{' '}
|
Synced to your instance server's settings.{' '}
|
||||||
<a
|
<a
|
||||||
href={`https://${instance}/`}
|
href={`https://${instance}/`}
|
||||||
|
@ -272,11 +307,14 @@ function Settings({ onClose }) {
|
||||||
>
|
>
|
||||||
Go to your instance ({instance}) for more settings.
|
Go to your instance ({instance}) for more settings.
|
||||||
</a>
|
</a>
|
||||||
|
</Trans>
|
||||||
</small>
|
</small>
|
||||||
</p>
|
</p>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<h3>Experiments</h3>
|
<h3>
|
||||||
|
<Trans>Experiments</Trans>
|
||||||
|
</h3>
|
||||||
<section>
|
<section>
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
|
@ -288,7 +326,7 @@ function Settings({ onClose }) {
|
||||||
states.settings.autoRefresh = e.target.checked;
|
states.settings.autoRefresh = e.target.checked;
|
||||||
}}
|
}}
|
||||||
/>{' '}
|
/>{' '}
|
||||||
Auto refresh timeline posts
|
<Trans>Auto refresh timeline posts</Trans>
|
||||||
</label>
|
</label>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
|
@ -300,7 +338,7 @@ function Settings({ onClose }) {
|
||||||
states.settings.boostsCarousel = e.target.checked;
|
states.settings.boostsCarousel = e.target.checked;
|
||||||
}}
|
}}
|
||||||
/>{' '}
|
/>{' '}
|
||||||
Boosts carousel
|
<Trans>Boosts carousel</Trans>
|
||||||
</label>
|
</label>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
|
@ -316,7 +354,7 @@ function Settings({ onClose }) {
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>{' '}
|
/>{' '}
|
||||||
Post translation
|
<Trans>Post translation</Trans>
|
||||||
</label>
|
</label>
|
||||||
<div
|
<div
|
||||||
class={`sub-section ${
|
class={`sub-section ${
|
||||||
|
@ -327,7 +365,7 @@ function Settings({ onClose }) {
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<label>
|
<label>
|
||||||
Translate to{' '}
|
<Trans>Translate to </Trans>
|
||||||
<select
|
<select
|
||||||
value={targetLanguage || ''}
|
value={targetLanguage || ''}
|
||||||
disabled={!snapStates.settings.contentTranslation}
|
disabled={!snapStates.settings.contentTranslation}
|
||||||
|
@ -337,33 +375,51 @@ function Settings({ onClose }) {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<option value="">
|
<option value="">
|
||||||
|
<Trans>
|
||||||
System language ({systemTargetLanguageText})
|
System language ({systemTargetLanguageText})
|
||||||
|
</Trans>
|
||||||
</option>
|
</option>
|
||||||
<option disabled>──────────</option>
|
<option disabled>──────────</option>
|
||||||
{targetLanguages.map((lang) => (
|
{targetLanguages.map((lang) => {
|
||||||
<option value={lang.code}>{lang.name}</option>
|
const common = localeCode2Text({
|
||||||
))}
|
code: lang.code,
|
||||||
|
fallback: lang.name,
|
||||||
|
});
|
||||||
|
const native = localeCode2Text({
|
||||||
|
code: lang.code,
|
||||||
|
locale: lang.code,
|
||||||
|
});
|
||||||
|
const same = !native || common === native;
|
||||||
|
return (
|
||||||
|
<option value={lang.code}>
|
||||||
|
{same ? common : `${common} (${native})`}
|
||||||
|
</option>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<hr />
|
<hr />
|
||||||
<p class="checkbox-fieldset">
|
<div class="checkbox-fieldset">
|
||||||
Hide "Translate" button for
|
<Plural
|
||||||
{snapStates.settings.contentTranslationHideLanguages.length >
|
value={
|
||||||
0 && (
|
snapStates.settings.contentTranslationHideLanguages.length
|
||||||
<>
|
|
||||||
{' '}
|
|
||||||
(
|
|
||||||
{
|
|
||||||
snapStates.settings.contentTranslationHideLanguages
|
|
||||||
.length
|
|
||||||
}
|
}
|
||||||
)
|
_0={`Hide "Translate" button for:`}
|
||||||
</>
|
other={`Hide "Translate" button for (#):`}
|
||||||
)}
|
/>
|
||||||
:
|
|
||||||
<div class="checkbox-fields">
|
<div class="checkbox-fields">
|
||||||
{targetLanguages.map((lang) => (
|
{targetLanguages.map((lang) => {
|
||||||
|
const common = localeCode2Text({
|
||||||
|
code: lang.code,
|
||||||
|
fallback: lang.name,
|
||||||
|
});
|
||||||
|
const native = localeCode2Text({
|
||||||
|
code: lang.code,
|
||||||
|
locale: lang.code,
|
||||||
|
});
|
||||||
|
const same = !native || common === native;
|
||||||
|
return (
|
||||||
<label>
|
<label>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
|
@ -384,13 +440,15 @@ function Settings({ onClose }) {
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>{' '}
|
/>{' '}
|
||||||
{lang.name}
|
{same ? common : `${common} (${native})`}
|
||||||
</label>
|
</label>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</p>
|
|
||||||
<p class="insignificant">
|
<p class="insignificant">
|
||||||
<small>
|
<small>
|
||||||
|
<Trans>
|
||||||
Note: This feature uses external translation services,
|
Note: This feature uses external translation services,
|
||||||
powered by{' '}
|
powered by{' '}
|
||||||
<a
|
<a
|
||||||
|
@ -409,6 +467,7 @@ function Settings({ onClose }) {
|
||||||
Lingva Translate
|
Lingva Translate
|
||||||
</a>
|
</a>
|
||||||
.
|
.
|
||||||
|
</Trans>
|
||||||
</small>
|
</small>
|
||||||
</p>
|
</p>
|
||||||
<hr />
|
<hr />
|
||||||
|
@ -423,13 +482,15 @@ function Settings({ onClose }) {
|
||||||
e.target.checked;
|
e.target.checked;
|
||||||
}}
|
}}
|
||||||
/>{' '}
|
/>{' '}
|
||||||
Auto inline translation
|
<Trans>Auto inline translation</Trans>
|
||||||
</label>
|
</label>
|
||||||
<p class="insignificant">
|
<p class="insignificant">
|
||||||
<small>
|
<small>
|
||||||
Automatically show translation for posts in timeline. Only
|
<Trans>
|
||||||
works for <b>short</b> posts without content warning,
|
Automatically show translation for posts in timeline.
|
||||||
media and poll.
|
Only works for <b>short</b> posts without content
|
||||||
|
warning, media and poll.
|
||||||
|
</Trans>
|
||||||
</small>
|
</small>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -445,12 +506,13 @@ function Settings({ onClose }) {
|
||||||
states.settings.composerGIFPicker = e.target.checked;
|
states.settings.composerGIFPicker = e.target.checked;
|
||||||
}}
|
}}
|
||||||
/>{' '}
|
/>{' '}
|
||||||
GIF Picker for composer
|
<Trans>GIF Picker for composer</Trans>
|
||||||
</label>
|
</label>
|
||||||
<div class="sub-section insignificant">
|
<div class="sub-section insignificant">
|
||||||
<small>
|
<small>
|
||||||
Note: This feature uses external GIF search service, powered
|
<Trans>
|
||||||
by{' '}
|
Note: This feature uses external GIF search service,
|
||||||
|
powered by{' '}
|
||||||
<a
|
<a
|
||||||
href="https://developers.giphy.com/"
|
href="https://developers.giphy.com/"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
@ -460,8 +522,9 @@ function Settings({ onClose }) {
|
||||||
</a>
|
</a>
|
||||||
. G-rated (suitable for viewing by all ages), tracking
|
. G-rated (suitable for viewing by all ages), tracking
|
||||||
parameters are stripped, referrer information is omitted
|
parameters are stripped, referrer information is omitted
|
||||||
from requests, but search queries and IP address information
|
from requests, but search queries and IP address
|
||||||
will still reach their servers.
|
information will still reach their servers.
|
||||||
|
</Trans>
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
@ -476,14 +539,19 @@ function Settings({ onClose }) {
|
||||||
states.settings.mediaAltGenerator = e.target.checked;
|
states.settings.mediaAltGenerator = e.target.checked;
|
||||||
}}
|
}}
|
||||||
/>{' '}
|
/>{' '}
|
||||||
Image description generator{' '}
|
<Trans>Image description generator</Trans>{' '}
|
||||||
<Icon icon="sparkles2" class="more-insignificant" />
|
<Icon icon="sparkles2" class="more-insignificant" />
|
||||||
</label>
|
</label>
|
||||||
<div class="sub-section insignificant">
|
<div class="sub-section insignificant">
|
||||||
<small>Only for new images while composing new posts.</small>
|
<small>
|
||||||
|
<Trans>
|
||||||
|
Only for new images while composing new posts.
|
||||||
|
</Trans>
|
||||||
|
</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="sub-section insignificant">
|
<div class="sub-section insignificant">
|
||||||
<small>
|
<small>
|
||||||
|
<Trans>
|
||||||
Note: This feature uses external AI service, powered by{' '}
|
Note: This feature uses external AI service, powered by{' '}
|
||||||
<a
|
<a
|
||||||
href="https://github.com/cheeaun/img-alt-api"
|
href="https://github.com/cheeaun/img-alt-api"
|
||||||
|
@ -493,6 +561,7 @@ function Settings({ onClose }) {
|
||||||
img-alt-api
|
img-alt-api
|
||||||
</a>
|
</a>
|
||||||
. May not work well. Only for images and in English.
|
. May not work well. Only for images and in English.
|
||||||
|
</Trans>
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
@ -508,12 +577,14 @@ function Settings({ onClose }) {
|
||||||
e.target.checked;
|
e.target.checked;
|
||||||
}}
|
}}
|
||||||
/>{' '}
|
/>{' '}
|
||||||
Server-side grouped notifications
|
<Trans>Server-side grouped notifications</Trans>
|
||||||
</label>
|
</label>
|
||||||
<div class="sub-section insignificant">
|
<div class="sub-section insignificant">
|
||||||
<small>
|
<small>
|
||||||
|
<Trans>
|
||||||
Alpha-stage feature. Potentially improved grouping window
|
Alpha-stage feature. Potentially improved grouping window
|
||||||
but basic grouping logic.
|
but basic grouping logic.
|
||||||
|
</Trans>
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
@ -531,22 +602,26 @@ function Settings({ onClose }) {
|
||||||
e.target.checked;
|
e.target.checked;
|
||||||
}}
|
}}
|
||||||
/>{' '}
|
/>{' '}
|
||||||
"Cloud" import/export for shortcuts settings{' '}
|
<Trans>"Cloud" import/export for shortcuts settings</Trans>{' '}
|
||||||
<Icon icon="cloud" class="more-insignificant" />
|
<Icon icon="cloud" class="more-insignificant" />
|
||||||
</label>
|
</label>
|
||||||
<div class="sub-section insignificant">
|
<div class="sub-section insignificant">
|
||||||
<small>
|
<small>
|
||||||
|
<Trans>
|
||||||
⚠️⚠️⚠️ Very experimental.
|
⚠️⚠️⚠️ Very experimental.
|
||||||
<br />
|
<br />
|
||||||
Stored in your own profile’s notes. Profile (private) notes
|
Stored in your own profile’s notes. Profile (private)
|
||||||
are mainly used for other profiles, and hidden for own
|
notes are mainly used for other profiles, and hidden for
|
||||||
profile.
|
own profile.
|
||||||
|
</Trans>
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="sub-section insignificant">
|
<div class="sub-section insignificant">
|
||||||
<small>
|
<small>
|
||||||
Note: This feature uses currently-logged-in instance server
|
<Trans>
|
||||||
API.
|
Note: This feature uses currently-logged-in instance
|
||||||
|
server API.
|
||||||
|
</Trans>
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
@ -560,15 +635,19 @@ function Settings({ onClose }) {
|
||||||
states.settings.cloakMode = e.target.checked;
|
states.settings.cloakMode = e.target.checked;
|
||||||
}}
|
}}
|
||||||
/>{' '}
|
/>{' '}
|
||||||
|
<Trans>
|
||||||
Cloak mode{' '}
|
Cloak mode{' '}
|
||||||
<span class="insignificant">
|
<span class="insignificant">
|
||||||
(<samp>Text</samp> → <samp>████</samp>)
|
(<samp>Text</samp> → <samp>████</samp>)
|
||||||
</span>
|
</span>
|
||||||
|
</Trans>
|
||||||
</label>
|
</label>
|
||||||
<div class="sub-section insignificant">
|
<div class="sub-section insignificant">
|
||||||
<small>
|
<small>
|
||||||
|
<Trans>
|
||||||
Replace text as blocks, useful when taking screenshots, for
|
Replace text as blocks, useful when taking screenshots, for
|
||||||
privacy reasons.
|
privacy reasons.
|
||||||
|
</Trans>
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
@ -582,14 +661,16 @@ function Settings({ onClose }) {
|
||||||
states.showSettings = false;
|
states.showSettings = false;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Unsent drafts
|
<Trans>Unsent drafts</Trans>
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
)}
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
{authenticated && <PushNotificationsSection onClose={onClose} />}
|
{authenticated && <PushNotificationsSection onClose={onClose} />}
|
||||||
<h3>About</h3>
|
<h3>
|
||||||
|
<Trans>About</Trans>
|
||||||
|
</h3>
|
||||||
<section>
|
<section>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
|
@ -627,6 +708,7 @@ function Settings({ onClose }) {
|
||||||
@phanpy
|
@phanpy
|
||||||
</a>
|
</a>
|
||||||
<br />
|
<br />
|
||||||
|
<Trans>
|
||||||
<a
|
<a
|
||||||
href="https://github.com/cheeaun/phanpy"
|
href="https://github.com/cheeaun/phanpy"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
@ -646,6 +728,7 @@ function Settings({ onClose }) {
|
||||||
>
|
>
|
||||||
@cheeaun
|
@cheeaun
|
||||||
</a>
|
</a>
|
||||||
|
</Trans>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p>
|
<p>
|
||||||
|
@ -654,7 +737,7 @@ function Settings({ onClose }) {
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
>
|
>
|
||||||
Sponsor
|
<Trans>Sponsor</Trans>
|
||||||
</a>{' '}
|
</a>{' '}
|
||||||
·{' '}
|
·{' '}
|
||||||
<a
|
<a
|
||||||
|
@ -662,7 +745,7 @@ function Settings({ onClose }) {
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
>
|
>
|
||||||
Donate
|
<Trans>Donate</Trans>
|
||||||
</a>{' '}
|
</a>{' '}
|
||||||
·{' '}
|
·{' '}
|
||||||
<a
|
<a
|
||||||
|
@ -670,18 +753,21 @@ function Settings({ onClose }) {
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
>
|
>
|
||||||
Privacy Policy
|
<Trans>Privacy Policy</Trans>
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
{__BUILD_TIME__ && (
|
{__BUILD_TIME__ && (
|
||||||
<p>
|
<p>
|
||||||
{WEBSITE && (
|
{WEBSITE && (
|
||||||
<>
|
<>
|
||||||
|
<Trans>
|
||||||
<span class="insignificant">Site:</span>{' '}
|
<span class="insignificant">Site:</span>{' '}
|
||||||
{WEBSITE.replace(/https?:\/\//g, '').replace(/\/$/, '')}
|
{WEBSITE.replace(/https?:\/\//g, '').replace(/\/$/, '')}
|
||||||
|
</Trans>
|
||||||
<br />
|
<br />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
<Trans>
|
||||||
<span class="insignificant">Version:</span>{' '}
|
<span class="insignificant">Version:</span>{' '}
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
|
@ -696,10 +782,10 @@ function Settings({ onClose }) {
|
||||||
// Copy to clipboard
|
// Copy to clipboard
|
||||||
try {
|
try {
|
||||||
navigator.clipboard.writeText(e.target.value);
|
navigator.clipboard.writeText(e.target.value);
|
||||||
showToast('Version string copied');
|
showToast(t`Version string copied`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn(e);
|
console.warn(e);
|
||||||
showToast('Unable to copy version string');
|
showToast(t`Unable to copy version string`);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>{' '}
|
/>{' '}
|
||||||
|
@ -716,6 +802,7 @@ function Settings({ onClose }) {
|
||||||
)
|
)
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
</Trans>
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
@ -823,24 +910,26 @@ function PushNotificationsSection({ onClose }) {
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.warn(err);
|
console.warn(err);
|
||||||
alert('Failed to update subscription. Please try again.');
|
alert(t`Failed to update subscription. Please try again.`);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
updateSubscription(params).catch((err) => {
|
updateSubscription(params).catch((err) => {
|
||||||
console.warn(err);
|
console.warn(err);
|
||||||
alert('Failed to update subscription. Please try again.');
|
alert(t`Failed to update subscription. Please try again.`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
removeSubscription().catch((err) => {
|
removeSubscription().catch((err) => {
|
||||||
console.warn(err);
|
console.warn(err);
|
||||||
alert('Failed to remove subscription. Please try again.');
|
alert(t`Failed to remove subscription. Please try again.`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, 100);
|
}, 100);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<h3>Push Notifications (beta)</h3>
|
<h3>
|
||||||
|
<Trans>Push Notifications (beta)</Trans>
|
||||||
|
</h3>
|
||||||
<section>
|
<section>
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
|
@ -861,7 +950,7 @@ function PushNotificationsSection({ onClose }) {
|
||||||
setAllowNotifications(false);
|
setAllowNotifications(false);
|
||||||
if (permission === 'denied') {
|
if (permission === 'denied') {
|
||||||
alert(
|
alert(
|
||||||
'Push notifications are blocked. Please enable them in your browser settings.',
|
t`Push notifications are blocked. Please enable them in your browser settings.`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -870,6 +959,7 @@ function PushNotificationsSection({ onClose }) {
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>{' '}
|
/>{' '}
|
||||||
|
<Trans>
|
||||||
Allow from{' '}
|
Allow from{' '}
|
||||||
<select
|
<select
|
||||||
name="policy"
|
name="policy"
|
||||||
|
@ -878,20 +968,21 @@ function PushNotificationsSection({ onClose }) {
|
||||||
{[
|
{[
|
||||||
{
|
{
|
||||||
value: 'all',
|
value: 'all',
|
||||||
label: 'anyone',
|
label: t`anyone`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'followed',
|
value: 'followed',
|
||||||
label: 'people I follow',
|
label: t`people I follow`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'follower',
|
value: 'follower',
|
||||||
label: 'followers',
|
label: t`followers`,
|
||||||
},
|
},
|
||||||
].map((type) => (
|
].map((type) => (
|
||||||
<option value={type.value}>{type.label}</option>
|
<option value={type.value}>{type.label}</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
|
</Trans>
|
||||||
</label>
|
</label>
|
||||||
<div
|
<div
|
||||||
class="shazam-container no-animation"
|
class="shazam-container no-animation"
|
||||||
|
@ -906,35 +997,35 @@ function PushNotificationsSection({ onClose }) {
|
||||||
{[
|
{[
|
||||||
{
|
{
|
||||||
value: 'mention',
|
value: 'mention',
|
||||||
label: 'Mentions',
|
label: t`Mentions`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'favourite',
|
value: 'favourite',
|
||||||
label: 'Likes',
|
label: t`Likes`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'reblog',
|
value: 'reblog',
|
||||||
label: 'Boosts',
|
label: t`Boosts`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'follow',
|
value: 'follow',
|
||||||
label: 'Follows',
|
label: t`Follows`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'followRequest',
|
value: 'followRequest',
|
||||||
label: 'Follow requests',
|
label: t`Follow requests`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'poll',
|
value: 'poll',
|
||||||
label: 'Polls',
|
label: t`Polls`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'update',
|
value: 'update',
|
||||||
label: 'Post edits',
|
label: t`Post edits`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'status',
|
value: 'status',
|
||||||
label: 'New posts',
|
label: t`New posts`,
|
||||||
},
|
},
|
||||||
].map((alert) => (
|
].map((alert) => (
|
||||||
<li>
|
<li>
|
||||||
|
@ -951,12 +1042,14 @@ function PushNotificationsSection({ onClose }) {
|
||||||
{needRelogin && (
|
{needRelogin && (
|
||||||
<div class="sub-section">
|
<div class="sub-section">
|
||||||
<p>
|
<p>
|
||||||
Push permission was not granted since your last login. You'll
|
<Trans>
|
||||||
need to{' '}
|
Push permission was not granted since your last login.
|
||||||
|
You'll need to{' '}
|
||||||
<Link to={`/login?instance=${instance}`} onClick={onClose}>
|
<Link to={`/login?instance=${instance}`} onClick={onClose}>
|
||||||
<b>log in</b> again to grant push permission
|
<b>log in</b> again to grant push permission
|
||||||
</Link>
|
</Link>
|
||||||
.
|
.
|
||||||
|
</Trans>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@ -965,7 +1058,9 @@ function PushNotificationsSection({ onClose }) {
|
||||||
</section>
|
</section>
|
||||||
<p class="section-postnote">
|
<p class="section-postnote">
|
||||||
<small>
|
<small>
|
||||||
|
<Trans>
|
||||||
NOTE: Push notifications only work for <b>one account</b>.
|
NOTE: Push notifications only work for <b>one account</b>.
|
||||||
|
</Trans>
|
||||||
</small>
|
</small>
|
||||||
</p>
|
</p>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import './status.css';
|
import './status.css';
|
||||||
|
|
||||||
|
import { Plural, t, Trans } from '@lingui/macro';
|
||||||
import { Menu, MenuDivider, MenuHeader, MenuItem } from '@szhsin/react-menu';
|
import { Menu, MenuDivider, MenuHeader, MenuItem } from '@szhsin/react-menu';
|
||||||
import debounce from 'just-debounce-it';
|
import debounce from 'just-debounce-it';
|
||||||
import pRetry from 'p-retry';
|
import pRetry from 'p-retry';
|
||||||
|
@ -561,7 +562,7 @@ function StatusThread({ id, closeLink = '/', instance: propInstance }) {
|
||||||
useTitle(
|
useTitle(
|
||||||
heroDisplayName && heroContentText
|
heroDisplayName && heroContentText
|
||||||
? `${heroDisplayName}: "${heroContentText}"`
|
? `${heroDisplayName}: "${heroContentText}"`
|
||||||
: 'Status',
|
: t`Post`,
|
||||||
'/:instance?/s/:id',
|
'/:instance?/s/:id',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -782,19 +783,23 @@ function StatusThread({ id, closeLink = '/', instance: propInstance }) {
|
||||||
{uiState !== 'loading' && !authenticated ? (
|
{uiState !== 'loading' && !authenticated ? (
|
||||||
<div class="post-status-banner">
|
<div class="post-status-banner">
|
||||||
<p>
|
<p>
|
||||||
|
<Trans>
|
||||||
You're not logged in. Interactions (reply, boost, etc) are
|
You're not logged in. Interactions (reply, boost, etc) are
|
||||||
not possible.
|
not possible.
|
||||||
|
</Trans>
|
||||||
</p>
|
</p>
|
||||||
<Link to="/login" class="button">
|
<Link to="/login" class="button">
|
||||||
Log in
|
<Trans>Log in</Trans>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
!sameInstance && (
|
!sameInstance && (
|
||||||
<div class="post-status-banner">
|
<div class="post-status-banner">
|
||||||
<p>
|
<p>
|
||||||
|
<Trans>
|
||||||
This post is from another instance (<b>{instance}</b>).
|
This post is from another instance (<b>{instance}</b>).
|
||||||
Interactions (reply, boost, etc) are not possible.
|
Interactions (reply, boost, etc) are not possible.
|
||||||
|
</Trans>
|
||||||
</p>
|
</p>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
@ -819,14 +824,16 @@ function StatusThread({ id, closeLink = '/', instance: propInstance }) {
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setUIState('default');
|
setUIState('default');
|
||||||
alert('Error: ' + e);
|
alert(t`Error: ${e}`);
|
||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon icon="transfer" /> Switch to my instance to enable
|
<Icon icon="transfer" />{' '}
|
||||||
interactions
|
<Trans>
|
||||||
|
Switch to my instance to enable interactions
|
||||||
|
</Trans>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
@ -882,7 +889,7 @@ function StatusThread({ id, closeLink = '/', instance: propInstance }) {
|
||||||
)}
|
)}
|
||||||
{ancestor && repliesCount > 1 && (
|
{ancestor && repliesCount > 1 && (
|
||||||
<div class="replies-link">
|
<div class="replies-link">
|
||||||
<Icon icon="comment2" />{' '}
|
<Icon icon="comment2" alt={t`Replies`} />{' '}
|
||||||
<span title={repliesCount}>
|
<span title={repliesCount}>
|
||||||
{shortenNumber(repliesCount)}
|
{shortenNumber(repliesCount)}
|
||||||
</span>
|
</span>
|
||||||
|
@ -926,7 +933,7 @@ function StatusThread({ id, closeLink = '/', instance: propInstance }) {
|
||||||
!!heroStatus?.repliesCount &&
|
!!heroStatus?.repliesCount &&
|
||||||
!hasDescendants && (
|
!hasDescendants && (
|
||||||
<div class="status-error">
|
<div class="status-error">
|
||||||
Unable to load replies.
|
<Trans>Unable to load replies.</Trans>
|
||||||
<br />
|
<br />
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
@ -935,7 +942,7 @@ function StatusThread({ id, closeLink = '/', instance: propInstance }) {
|
||||||
states.reloadStatusPage++;
|
states.reloadStatusPage++;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Try again
|
<Trans>Try again</Trans>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@ -1038,7 +1045,7 @@ function StatusThread({ id, closeLink = '/', instance: propInstance }) {
|
||||||
history.back();
|
history.back();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon icon="chevron-left" size="xl" />
|
<Icon icon="chevron-left" size="xl" alt={t`Back`} />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{!heroInView && heroStatus && uiState !== 'loading' ? (
|
{!heroInView && heroStatus && uiState !== 'loading' ? (
|
||||||
|
@ -1069,7 +1076,7 @@ function StatusThread({ id, closeLink = '/', instance: propInstance }) {
|
||||||
block: 'start',
|
block: 'start',
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
title="Go to main post"
|
title={t`Go to main post`}
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
icon={heroPointer === 'down' ? 'arrow-down' : 'arrow-up'}
|
icon={heroPointer === 'down' ? 'arrow-down' : 'arrow-up'}
|
||||||
|
@ -1092,7 +1099,7 @@ function StatusThread({ id, closeLink = '/', instance: propInstance }) {
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
hidden={!ancestors.length || reachTopPost}
|
hidden={!ancestors.length || reachTopPost}
|
||||||
title={`${ancestors.length} posts above ‒ Go to top`}
|
title={t`${ancestors.length} posts above ‒ Go to top`}
|
||||||
>
|
>
|
||||||
<Icon icon="arrow-up" />
|
<Icon icon="arrow-up" />
|
||||||
{ancestors
|
{ancestors
|
||||||
|
@ -1135,7 +1142,7 @@ function StatusThread({ id, closeLink = '/', instance: propInstance }) {
|
||||||
searchParams.delete('view');
|
searchParams.delete('view');
|
||||||
setSearchParams(searchParams);
|
setSearchParams(searchParams);
|
||||||
}}
|
}}
|
||||||
title="Switch to Side Peek view"
|
title={t`Switch to Side Peek view`}
|
||||||
>
|
>
|
||||||
<Icon icon="layout4" size="l" />
|
<Icon icon="layout4" size="l" />
|
||||||
</button>
|
</button>
|
||||||
|
@ -1148,7 +1155,7 @@ function StatusThread({ id, closeLink = '/', instance: propInstance }) {
|
||||||
setShowRefresh(false);
|
setShowRefresh(false);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon icon="refresh" size="l" />
|
<Icon icon="refresh" size="l" alt={t`Refresh`} />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<Menu2
|
<Menu2
|
||||||
|
@ -1159,7 +1166,7 @@ function StatusThread({ id, closeLink = '/', instance: propInstance }) {
|
||||||
}}
|
}}
|
||||||
menuButton={
|
menuButton={
|
||||||
<button type="button" class="button plain4">
|
<button type="button" class="button plain4">
|
||||||
<Icon icon="more" alt="Actions" size="xl" />
|
<Icon icon="more" alt={t`More`} size="xl" />
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
@ -1170,7 +1177,9 @@ function StatusThread({ id, closeLink = '/', instance: propInstance }) {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon icon="refresh" />
|
<Icon icon="refresh" />
|
||||||
<span>Refresh</span>
|
<span>
|
||||||
|
<Trans>Refresh</Trans>
|
||||||
|
</span>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
className="menu-switch-view"
|
className="menu-switch-view"
|
||||||
|
@ -1195,7 +1204,9 @@ function StatusThread({ id, closeLink = '/', instance: propInstance }) {
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<span>
|
<span>
|
||||||
Switch to {viewMode === 'full' ? 'Side Peek' : 'Full'} view
|
{viewMode === 'full'
|
||||||
|
? t`Switch to Side Peek view`
|
||||||
|
: t`Switch to Full view`}
|
||||||
</span>
|
</span>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
|
@ -1211,10 +1222,15 @@ function StatusThread({ id, closeLink = '/', instance: propInstance }) {
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon icon="eye-open" /> <span>Show all sensitive content</span>
|
<Icon icon="eye-open" />{' '}
|
||||||
|
<span>
|
||||||
|
<Trans>Show all sensitive content</Trans>
|
||||||
|
</span>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuDivider />
|
<MenuDivider />
|
||||||
<MenuHeader className="plain">Experimental</MenuHeader>
|
<MenuHeader className="plain">
|
||||||
|
<Trans>Experimental</Trans>
|
||||||
|
</MenuHeader>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
disabled={!postInstance || postSameInstance}
|
disabled={!postInstance || postSameInstance}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
@ -1222,26 +1238,22 @@ function StatusThread({ id, closeLink = '/', instance: propInstance }) {
|
||||||
if (statusURL) {
|
if (statusURL) {
|
||||||
location.hash = statusURL;
|
location.hash = statusURL;
|
||||||
} else {
|
} else {
|
||||||
alert('Unable to switch');
|
alert(t`Unable to switch`);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon icon="transfer" />
|
<Icon icon="transfer" />
|
||||||
<small class="menu-double-lines">
|
<small class="menu-double-lines">
|
||||||
Switch to post's instance
|
{postInstance
|
||||||
{postInstance ? (
|
? t`Switch to post's instance (${punycode.toUnicode(
|
||||||
<>
|
postInstance,
|
||||||
{' '}
|
)})`
|
||||||
(<b>{punycode.toUnicode(postInstance)}</b>)
|
: t`Switch to post's instance`}
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
''
|
|
||||||
)}
|
|
||||||
</small>
|
</small>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</Menu2>
|
</Menu2>
|
||||||
<Link class="button plain deck-close" to={closeLink}>
|
<Link class="button plain deck-close" to={closeLink}>
|
||||||
<Icon icon="x" size="xl" />
|
<Icon icon="x" size="xl" alt={t`Close`} />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1274,7 +1286,7 @@ function StatusThread({ id, closeLink = '/', instance: propInstance }) {
|
||||||
))}
|
))}
|
||||||
</div>{' '}
|
</div>{' '}
|
||||||
<div class="ib">
|
<div class="ib">
|
||||||
Show more…{' '}
|
<Trans>Show more…</Trans>{' '}
|
||||||
<span class="tag">
|
<span class="tag">
|
||||||
{showMore > LIMIT ? `${LIMIT}+` : showMore}
|
{showMore > LIMIT ? `${LIMIT}+` : showMore}
|
||||||
</span>
|
</span>
|
||||||
|
@ -1294,7 +1306,7 @@ function StatusThread({ id, closeLink = '/', instance: propInstance }) {
|
||||||
)}
|
)}
|
||||||
{uiState === 'error' && (
|
{uiState === 'error' && (
|
||||||
<p class="ui-state">
|
<p class="ui-state">
|
||||||
Unable to load post
|
<Trans>Unable to load post</Trans>
|
||||||
<br />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
<button
|
<button
|
||||||
|
@ -1303,7 +1315,7 @@ function StatusThread({ id, closeLink = '/', instance: propInstance }) {
|
||||||
states.reloadStatusPage++;
|
states.reloadStatusPage++;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Try again
|
<Trans>Try again</Trans>
|
||||||
</button>
|
</button>
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
@ -1411,20 +1423,36 @@ function SubComments({
|
||||||
</span>
|
</span>
|
||||||
<span class="replies-counts">
|
<span class="replies-counts">
|
||||||
<b>
|
<b>
|
||||||
<span title={replies.length}>{shortenNumber(replies.length)}</span>{' '}
|
<Plural
|
||||||
repl
|
value={replies.length}
|
||||||
{replies.length === 1 ? 'y' : 'ies'}
|
one="# reply"
|
||||||
|
other={
|
||||||
|
<Trans>
|
||||||
|
<span title={replies.length}>
|
||||||
|
{shortenNumber(replies.length)}
|
||||||
|
</span>{' '}
|
||||||
|
replies
|
||||||
|
</Trans>
|
||||||
|
}
|
||||||
|
/>
|
||||||
</b>
|
</b>
|
||||||
{!sameCount && totalComments > 1 && (
|
{!sameCount && totalComments > 1 && (
|
||||||
<>
|
<>
|
||||||
{' '}
|
{' '}
|
||||||
·{' '}
|
·{' '}
|
||||||
<span>
|
<span>
|
||||||
|
<Plural
|
||||||
|
value={totalComments}
|
||||||
|
one="# comment"
|
||||||
|
other={
|
||||||
|
<Trans>
|
||||||
<span title={totalComments}>
|
<span title={totalComments}>
|
||||||
{shortenNumber(totalComments)}
|
{shortenNumber(totalComments)}
|
||||||
</span>{' '}
|
</span>{' '}
|
||||||
comment
|
comments
|
||||||
{totalComments === 1 ? '' : 's'}
|
</Trans>
|
||||||
|
}
|
||||||
|
/>
|
||||||
</span>
|
</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
@ -1435,7 +1463,7 @@ function SubComments({
|
||||||
class="replies-parent-link"
|
class="replies-parent-link"
|
||||||
to={parentLink.to}
|
to={parentLink.to}
|
||||||
onClick={parentLink.onClick}
|
onClick={parentLink.onClick}
|
||||||
title="View post with its replies"
|
title={t`View post with its replies`}
|
||||||
>
|
>
|
||||||
»
|
»
|
||||||
</Link>
|
</Link>
|
||||||
|
@ -1463,7 +1491,7 @@ function SubComments({
|
||||||
/>
|
/>
|
||||||
{!r.replies?.length && r.repliesCount > 0 && (
|
{!r.replies?.length && r.repliesCount > 0 && (
|
||||||
<div class="replies-link">
|
<div class="replies-link">
|
||||||
<Icon icon="comment2" />{' '}
|
<Icon icon="comment2" alt={t`Replies`} />{' '}
|
||||||
<span title={r.repliesCount}>
|
<span title={r.repliesCount}>
|
||||||
{shortenNumber(r.repliesCount)}
|
{shortenNumber(r.repliesCount)}
|
||||||
</span>
|
</span>
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import '../components/links-bar.css';
|
import '../components/links-bar.css';
|
||||||
import './trending.css';
|
import './trending.css';
|
||||||
|
|
||||||
|
import { t, Trans } from '@lingui/macro';
|
||||||
import { MenuItem } from '@szhsin/react-menu';
|
import { MenuItem } from '@szhsin/react-menu';
|
||||||
import { getBlurHashAverageColor } from 'fast-blurhash';
|
import { getBlurHashAverageColor } from 'fast-blurhash';
|
||||||
import { useEffect, useMemo, useRef, useState } from 'preact/hooks';
|
import { useEffect, useMemo, useRef, useState } from 'preact/hooks';
|
||||||
|
@ -66,7 +67,7 @@ function Trending({ columnMode, ...props }) {
|
||||||
instance: props?.instance || params.instance,
|
instance: props?.instance || params.instance,
|
||||||
});
|
});
|
||||||
const { masto: currentMasto, instance: currentInstance } = api();
|
const { masto: currentMasto, instance: currentInstance } = api();
|
||||||
const title = `Trending (${instance})`;
|
const title = t`Trending (${instance})`;
|
||||||
useTitle(title, `/:instance?/trending`);
|
useTitle(title, `/:instance?/trending`);
|
||||||
// const navigate = useNavigate();
|
// const navigate = useNavigate();
|
||||||
const latestItem = useRef();
|
const latestItem = useRef();
|
||||||
|
@ -222,7 +223,9 @@ function Trending({ columnMode, ...props }) {
|
||||||
{!!links.length && (
|
{!!links.length && (
|
||||||
<div class="links-bar">
|
<div class="links-bar">
|
||||||
<header>
|
<header>
|
||||||
<h3>Trending News</h3>
|
<h3>
|
||||||
|
<Trans>Trending News</Trans>
|
||||||
|
</h3>
|
||||||
</header>
|
</header>
|
||||||
{links.map((link) => {
|
{links.map((link) => {
|
||||||
const {
|
const {
|
||||||
|
@ -339,7 +342,10 @@ function Trending({ columnMode, ...props }) {
|
||||||
}}
|
}}
|
||||||
disabled={url === currentLink}
|
disabled={url === currentLink}
|
||||||
>
|
>
|
||||||
<Icon icon="comment2" /> <span>Mentions</span>{' '}
|
<Icon icon="comment2" />{' '}
|
||||||
|
<span>
|
||||||
|
<Trans>Mentions</Trans>
|
||||||
|
</span>{' '}
|
||||||
<Icon icon="chevron-down" />
|
<Icon icon="chevron-down" />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
@ -365,21 +371,25 @@ function Trending({ columnMode, ...props }) {
|
||||||
setCurrentLink(null);
|
setCurrentLink(null);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon icon="x" />
|
<Icon icon="x" alt={t`Back to showing trending posts`} />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<p>
|
<p>
|
||||||
|
<Trans>
|
||||||
Showing posts mentioning{' '}
|
Showing posts mentioning{' '}
|
||||||
<span class="link-text">
|
<span class="link-text">
|
||||||
{currentLink
|
{currentLink
|
||||||
.replace(/^https?:\/\/(www\.)?/i, '')
|
.replace(/^https?:\/\/(www\.)?/i, '')
|
||||||
.replace(/\/$/, '')}
|
.replace(/\/$/, '')}
|
||||||
</span>
|
</span>
|
||||||
|
</Trans>
|
||||||
</p>
|
</p>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<p class="insignificant">Trending posts</p>
|
<p class="insignificant">
|
||||||
|
<Trans>Trending posts</Trans>
|
||||||
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@ -393,14 +403,16 @@ function Trending({ columnMode, ...props }) {
|
||||||
title={title}
|
title={title}
|
||||||
titleComponent={
|
titleComponent={
|
||||||
<h1 class="header-double-lines">
|
<h1 class="header-double-lines">
|
||||||
<b>Trending</b>
|
<b>
|
||||||
|
<Trans>Trending</Trans>
|
||||||
|
</b>
|
||||||
<div>{instance}</div>
|
<div>{instance}</div>
|
||||||
</h1>
|
</h1>
|
||||||
}
|
}
|
||||||
id="trending"
|
id="trending"
|
||||||
instance={instance}
|
instance={instance}
|
||||||
emptyText="No trending posts."
|
emptyText={t`No trending posts.`}
|
||||||
errorText="Unable to load posts"
|
errorText={t`Unable to load posts`}
|
||||||
fetchItems={hasCurrentLink ? fetchLinkMentions : fetchTrends}
|
fetchItems={hasCurrentLink ? fetchLinkMentions : fetchTrends}
|
||||||
checkForUpdates={hasCurrentLink ? undefined : checkForUpdates}
|
checkForUpdates={hasCurrentLink ? undefined : checkForUpdates}
|
||||||
checkForUpdatesInterval={5 * 60 * 1000} // 5 minutes
|
checkForUpdatesInterval={5 * 60 * 1000} // 5 minutes
|
||||||
|
@ -422,17 +434,17 @@ function Trending({ columnMode, ...props }) {
|
||||||
position="anchor"
|
position="anchor"
|
||||||
menuButton={
|
menuButton={
|
||||||
<button type="button" class="plain">
|
<button type="button" class="plain">
|
||||||
<Icon icon="more" size="l" />
|
<Icon icon="more" size="l" alt={t`More`} />
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
let newInstance = prompt(
|
let newInstance = prompt(
|
||||||
'Enter a new instance e.g. "mastodon.social"',
|
t`Enter a new instance e.g. "mastodon.social"`,
|
||||||
);
|
);
|
||||||
if (!/\./.test(newInstance)) {
|
if (!/\./.test(newInstance)) {
|
||||||
if (newInstance) alert('Invalid instance');
|
if (newInstance) alert(t`Invalid instance`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (newInstance) {
|
if (newInstance) {
|
||||||
|
@ -442,7 +454,10 @@ function Trending({ columnMode, ...props }) {
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon icon="bus" /> <span>Go to another instance…</span>
|
<Icon icon="bus" />{' '}
|
||||||
|
<span>
|
||||||
|
<Trans>Go to another instance…</Trans>
|
||||||
|
</span>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
{currentInstance !== instance && (
|
{currentInstance !== instance && (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
|
@ -452,7 +467,9 @@ function Trending({ columnMode, ...props }) {
|
||||||
>
|
>
|
||||||
<Icon icon="bus" />{' '}
|
<Icon icon="bus" />{' '}
|
||||||
<small class="menu-double-lines">
|
<small class="menu-double-lines">
|
||||||
|
<Trans>
|
||||||
Go to my instance (<b>{currentInstance}</b>)
|
Go to my instance (<b>{currentInstance}</b>)
|
||||||
|
</Trans>
|
||||||
</small>
|
</small>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import './welcome.css';
|
import './welcome.css';
|
||||||
|
|
||||||
|
import { t, Trans } from '@lingui/macro';
|
||||||
|
|
||||||
import boostsCarouselUrl from '../assets/features/boosts-carousel.jpg';
|
import boostsCarouselUrl from '../assets/features/boosts-carousel.jpg';
|
||||||
import groupedNotificationsUrl from '../assets/features/grouped-notifications.jpg';
|
import groupedNotificationsUrl from '../assets/features/grouped-notifications.jpg';
|
||||||
import multiColumnUrl from '../assets/features/multi-column.jpg';
|
import multiColumnUrl from '../assets/features/multi-column.jpg';
|
||||||
|
@ -8,6 +10,7 @@ import nestedCommentsThreadUrl from '../assets/features/nested-comments-thread.j
|
||||||
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 LangSelector from '../components/lang-selector';
|
||||||
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';
|
||||||
|
@ -46,7 +49,9 @@ function Welcome() {
|
||||||
/>
|
/>
|
||||||
<img src={logoText} alt="Phanpy" width="200" />
|
<img src={logoText} alt="Phanpy" width="200" />
|
||||||
</h1>
|
</h1>
|
||||||
<p class="desc">A minimalistic opinionated Mastodon web client.</p>
|
<p class="desc">
|
||||||
|
<Trans>A minimalistic opinionated Mastodon web client.</Trans>
|
||||||
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<Link
|
<Link
|
||||||
to={
|
to={
|
||||||
|
@ -56,22 +61,24 @@ function Welcome() {
|
||||||
}
|
}
|
||||||
class="button"
|
class="button"
|
||||||
>
|
>
|
||||||
{DEFAULT_INSTANCE ? 'Log in' : 'Log in with Mastodon'}
|
{DEFAULT_INSTANCE ? t`Log in` : t`Log in with Mastodon`}
|
||||||
</Link>
|
</Link>
|
||||||
</p>
|
</p>
|
||||||
{DEFAULT_INSTANCE && DEFAULT_INSTANCE_REGISTRATION_URL && (
|
{DEFAULT_INSTANCE && DEFAULT_INSTANCE_REGISTRATION_URL && (
|
||||||
<p>
|
<p>
|
||||||
<a href={DEFAULT_INSTANCE_REGISTRATION_URL} class="button plain5">
|
<a href={DEFAULT_INSTANCE_REGISTRATION_URL} class="button plain5">
|
||||||
Sign up
|
<Trans>Sign up</Trans>
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
{!DEFAULT_INSTANCE && (
|
{!DEFAULT_INSTANCE && (
|
||||||
<p class="insignificant">
|
<p class="insignificant">
|
||||||
<small>
|
<small>
|
||||||
|
<Trans>
|
||||||
Connect your existing Mastodon/Fediverse account.
|
Connect your existing Mastodon/Fediverse account.
|
||||||
<br />
|
<br />
|
||||||
Your credentials are not stored on this server.
|
Your credentials are not stored on this server.
|
||||||
|
</Trans>
|
||||||
</small>
|
</small>
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
@ -84,6 +91,7 @@ function Welcome() {
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
<p>
|
<p>
|
||||||
|
<Trans>
|
||||||
<a href="https://github.com/cheeaun/phanpy" target="_blank">
|
<a href="https://github.com/cheeaun/phanpy" target="_blank">
|
||||||
Built
|
Built
|
||||||
</a>{' '}
|
</a>{' '}
|
||||||
|
@ -103,62 +111,87 @@ function Welcome() {
|
||||||
Privacy Policy
|
Privacy Policy
|
||||||
</a>
|
</a>
|
||||||
.
|
.
|
||||||
|
</Trans>
|
||||||
</p>
|
</p>
|
||||||
|
<LangSelector />
|
||||||
</div>
|
</div>
|
||||||
<div id="why-container">
|
<div id="why-container">
|
||||||
<div class="sections">
|
<div class="sections">
|
||||||
<section>
|
<section>
|
||||||
<img
|
<img
|
||||||
src={boostsCarouselUrl}
|
src={boostsCarouselUrl}
|
||||||
alt="Screenshot of Boosts Carousel"
|
alt={t`Screenshot of Boosts Carousel`}
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
<h4>Boosts Carousel</h4>
|
<h4>
|
||||||
|
<Trans>Boosts Carousel</Trans>
|
||||||
|
</h4>
|
||||||
<p>
|
<p>
|
||||||
|
<Trans>
|
||||||
Visually separate original posts and re-shared posts (boosted
|
Visually separate original posts and re-shared posts (boosted
|
||||||
posts).
|
posts).
|
||||||
|
</Trans>
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
<section>
|
<section>
|
||||||
<img
|
<img
|
||||||
src={nestedCommentsThreadUrl}
|
src={nestedCommentsThreadUrl}
|
||||||
alt="Screenshot of nested comments thread"
|
alt={t`Screenshot of nested comments thread`}
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
<h4>Nested comments thread</h4>
|
<h4>
|
||||||
<p>Effortlessly follow conversations. Semi-collapsible replies.</p>
|
<Trans>Nested comments thread</Trans>
|
||||||
|
</h4>
|
||||||
|
<p>
|
||||||
|
<Trans>
|
||||||
|
Effortlessly follow conversations. Semi-collapsible replies.
|
||||||
|
</Trans>
|
||||||
|
</p>
|
||||||
</section>
|
</section>
|
||||||
<section>
|
<section>
|
||||||
<img
|
<img
|
||||||
src={groupedNotificationsUrl}
|
src={groupedNotificationsUrl}
|
||||||
alt="Screenshot of grouped notifications"
|
alt={t`Screenshot of grouped notifications`}
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
<h4>Grouped notifications</h4>
|
<h4>
|
||||||
|
<Trans>Grouped notifications</Trans>
|
||||||
|
</h4>
|
||||||
<p>
|
<p>
|
||||||
Similar notifications are grouped and collapsed to reduce clutter.
|
<Trans>
|
||||||
|
Similar notifications are grouped and collapsed to reduce
|
||||||
|
clutter.
|
||||||
|
</Trans>
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
<section>
|
<section>
|
||||||
<img
|
<img
|
||||||
src={multiColumnUrl}
|
src={multiColumnUrl}
|
||||||
alt="Screenshot of multi-column UI"
|
alt={t`Screenshot of multi-column UI`}
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
<h4>Single or multi-column</h4>
|
<h4>
|
||||||
|
<Trans>Single or multi-column</Trans>
|
||||||
|
</h4>
|
||||||
<p>
|
<p>
|
||||||
|
<Trans>
|
||||||
By default, single column for zen-mode seekers. Configurable
|
By default, single column for zen-mode seekers. Configurable
|
||||||
multi-column for power users.
|
multi-column for power users.
|
||||||
|
</Trans>
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
<section>
|
<section>
|
||||||
<img
|
<img
|
||||||
src={multiHashtagTimelineUrl}
|
src={multiHashtagTimelineUrl}
|
||||||
alt="Screenshot of multi-hashtag timeline with a form to add more hashtags"
|
alt={t`Screenshot of multi-hashtag timeline with a form to add more hashtags`}
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
<h4>Multi-hashtag timeline</h4>
|
<h4>
|
||||||
<p>Up to 5 hashtags combined into a single timeline.</p>
|
<Trans>Multi-hashtag timeline</Trans>
|
||||||
|
</h4>
|
||||||
|
<p>
|
||||||
|
<Trans>Up to 5 hashtags combined into a single timeline.</Trans>
|
||||||
|
</p>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
10
src/utils/i18n-duration.js
Normal file
10
src/utils/i18n-duration.js
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import { i18n } from '@lingui/core';
|
||||||
|
|
||||||
|
export default function i18nDuration(duration, unit) {
|
||||||
|
return () =>
|
||||||
|
i18n.number(duration, {
|
||||||
|
style: 'unit',
|
||||||
|
unit,
|
||||||
|
unitDisplay: 'long',
|
||||||
|
});
|
||||||
|
}
|
56
src/utils/lang.js
Normal file
56
src/utils/lang.js
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
import { i18n } from '@lingui/core';
|
||||||
|
import {
|
||||||
|
detect,
|
||||||
|
fromNavigator,
|
||||||
|
fromStorage,
|
||||||
|
fromUrl,
|
||||||
|
} from '@lingui/detect-locale';
|
||||||
|
import Locale from 'intl-locale-textinfo-polyfill';
|
||||||
|
|
||||||
|
import { messages } from '../locales/en.po';
|
||||||
|
import localeMatch from '../utils/locale-match';
|
||||||
|
|
||||||
|
const { PHANPY_DEFAULT_LANG } = import.meta.env;
|
||||||
|
|
||||||
|
export const DEFAULT_LANG = 'en';
|
||||||
|
export const LOCALES = [DEFAULT_LANG];
|
||||||
|
if (import.meta.env.DEV) {
|
||||||
|
LOCALES.push('pseudo-LOCALE');
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function activateLang(lang) {
|
||||||
|
if (!lang || lang === DEFAULT_LANG) {
|
||||||
|
i18n.loadAndActivate({ locale: DEFAULT_LANG, messages });
|
||||||
|
console.log('💬 ACTIVATE LANG', lang);
|
||||||
|
} else {
|
||||||
|
const { messages } = await import(`../locales/${lang}.po`);
|
||||||
|
i18n.loadAndActivate({ locale: lang, messages });
|
||||||
|
console.log('💬 ACTIVATE LANG', lang);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
i18n.on('change', () => {
|
||||||
|
const lang = i18n.locale;
|
||||||
|
if (lang) {
|
||||||
|
// LTR or RTL
|
||||||
|
const { direction } = new Locale(lang).textInfo;
|
||||||
|
document.documentElement.dir = direction;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export function initActivateLang() {
|
||||||
|
const lang = detect(
|
||||||
|
fromUrl('lang'),
|
||||||
|
fromStorage('lang'),
|
||||||
|
fromNavigator(),
|
||||||
|
PHANPY_DEFAULT_LANG,
|
||||||
|
DEFAULT_LANG,
|
||||||
|
);
|
||||||
|
const matchedLang = localeMatch(lang, LOCALES);
|
||||||
|
activateLang(matchedLang);
|
||||||
|
|
||||||
|
// const yes = confirm(t`Reload to apply language setting?`);
|
||||||
|
// if (yes) {
|
||||||
|
// window.location.reload();
|
||||||
|
// }
|
||||||
|
}
|
|
@ -1,15 +1,41 @@
|
||||||
|
import { i18n } from '@lingui/core';
|
||||||
|
|
||||||
import mem from './mem';
|
import mem from './mem';
|
||||||
|
|
||||||
const IntlDN = new Intl.DisplayNames(undefined, {
|
// Some codes are not supported by Intl.DisplayNames
|
||||||
|
// These are mapped to other codes as fallback
|
||||||
|
const codeMappings = {
|
||||||
|
'zh-YUE': 'YUE',
|
||||||
|
zh_HANT: 'zh-Hant',
|
||||||
|
};
|
||||||
|
|
||||||
|
const IntlDN = mem(
|
||||||
|
(locale) =>
|
||||||
|
new Intl.DisplayNames(locale || undefined, {
|
||||||
type: 'language',
|
type: 'language',
|
||||||
});
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
function _localeCode2Text(code) {
|
function _localeCode2Text(code) {
|
||||||
|
let locale;
|
||||||
|
let fallback;
|
||||||
|
if (typeof code === 'object') {
|
||||||
|
({ code, locale, fallback } = code);
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
return IntlDN.of(code);
|
const text = IntlDN(locale || i18n.locale).of(code);
|
||||||
|
if (text !== code) return text;
|
||||||
|
return fallback || '';
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
if (codeMappings[code]) {
|
||||||
return null;
|
try {
|
||||||
|
const text = IntlDN(locale || i18n.locale).of(codeMappings[code]);
|
||||||
|
if (text !== codeMappings[code]) return text;
|
||||||
|
return fallback || '';
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
console.warn(code, e);
|
||||||
|
return fallback || '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
|
import { i18n } from '@lingui/core';
|
||||||
|
|
||||||
import mem from './mem';
|
import mem from './mem';
|
||||||
|
|
||||||
const { locale } = new Intl.DateTimeFormat().resolvedOptions();
|
const defaultLocale = new Intl.DateTimeFormat().resolvedOptions().locale;
|
||||||
|
|
||||||
const _DateTimeFormat = (opts) => {
|
const _DateTimeFormat = (opts) => {
|
||||||
const { dateYear, hideTime, formatOpts } = opts || {};
|
const { locale, dateYear, hideTime, formatOpts } = opts || {};
|
||||||
|
const loc = locale && !/pseudo/i.test(locale) ? locale : defaultLocale;
|
||||||
const currentYear = new Date().getFullYear();
|
const currentYear = new Date().getFullYear();
|
||||||
return Intl.DateTimeFormat(locale, {
|
return Intl.DateTimeFormat(loc, {
|
||||||
// Show year if not current year
|
// Show year if not current year
|
||||||
year: dateYear === currentYear ? undefined : 'numeric',
|
year: dateYear === currentYear ? undefined : 'numeric',
|
||||||
month: 'short',
|
month: 'short',
|
||||||
|
@ -24,6 +27,7 @@ function niceDateTime(date, dtfOpts) {
|
||||||
}
|
}
|
||||||
const DTF = DateTimeFormat({
|
const DTF = DateTimeFormat({
|
||||||
dateYear: date.getFullYear(),
|
dateYear: date.getFullYear(),
|
||||||
|
locale: i18n.locale,
|
||||||
...dtfOpts,
|
...dtfOpts,
|
||||||
});
|
});
|
||||||
const dateText = DTF.format(date);
|
const dateText = DTF.format(date);
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { t, Trans } from '@lingui/macro';
|
||||||
|
|
||||||
export default function openCompose(opts) {
|
export default function openCompose(opts) {
|
||||||
const url = URL.parse('/compose/', window.location);
|
const url = URL.parse('/compose/', window.location);
|
||||||
const { width: screenWidth, height: screenHeight } = window.screen;
|
const { width: screenWidth, height: screenHeight } = window.screen;
|
||||||
|
@ -19,7 +21,7 @@ export default function openCompose(opts) {
|
||||||
|
|
||||||
newWin.__COMPOSE__ = opts;
|
newWin.__COMPOSE__ = opts;
|
||||||
} else {
|
} else {
|
||||||
alert('Looks like your browser is blocking popups.');
|
alert(t`Looks like your browser is blocking popups.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return newWin;
|
return newWin;
|
||||||
|
|
24
src/utils/pretty-bytes.js
Normal file
24
src/utils/pretty-bytes.js
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import { i18n } from '@lingui/core';
|
||||||
|
|
||||||
|
// https://tc39.es/ecma402/#table-sanctioned-single-unit-identifiers
|
||||||
|
const BYTES_UNITS = [
|
||||||
|
'byte',
|
||||||
|
'kilobyte',
|
||||||
|
'megabyte',
|
||||||
|
'gigabyte',
|
||||||
|
'terabyte',
|
||||||
|
'petabyte',
|
||||||
|
];
|
||||||
|
export default function prettyBytes(bytes) {
|
||||||
|
const unitIndex = Math.min(
|
||||||
|
Math.floor(Math.log2(bytes) / 10),
|
||||||
|
BYTES_UNITS.length - 1,
|
||||||
|
);
|
||||||
|
const value = bytes / 1024 ** unitIndex;
|
||||||
|
return i18n.number(value, {
|
||||||
|
style: 'unit',
|
||||||
|
unit: BYTES_UNITS[unitIndex],
|
||||||
|
unitDisplay: 'narrow',
|
||||||
|
maximumFractionDigits: 0,
|
||||||
|
});
|
||||||
|
}
|
|
@ -1,6 +1,8 @@
|
||||||
const { locale } = Intl.NumberFormat().resolvedOptions();
|
import { i18n } from '@lingui/core';
|
||||||
const shortenNumber = Intl.NumberFormat(locale, {
|
|
||||||
|
export default function shortenNumber(num) {
|
||||||
|
return i18n.number(num, {
|
||||||
notation: 'compact',
|
notation: 'compact',
|
||||||
roundingMode: 'floor',
|
roundingMode: 'floor',
|
||||||
}).format;
|
});
|
||||||
export default shortenNumber;
|
}
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { t, Trans } from '@lingui/macro';
|
||||||
|
|
||||||
import openOSK from './open-osk';
|
import openOSK from './open-osk';
|
||||||
import showToast from './show-toast';
|
import showToast from './show-toast';
|
||||||
import states from './states';
|
import states from './states';
|
||||||
|
@ -11,12 +13,12 @@ export default function showCompose(opts) {
|
||||||
if (states.composerState.minimized) {
|
if (states.composerState.minimized) {
|
||||||
showToast({
|
showToast({
|
||||||
duration: TOAST_DURATION,
|
duration: TOAST_DURATION,
|
||||||
text: `A draft post is currently minimized. Post or discard it before creating a new one.`,
|
text: t`A draft post is currently minimized. Post or discard it before creating a new one.`,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
showToast({
|
showToast({
|
||||||
duration: TOAST_DURATION,
|
duration: TOAST_DURATION,
|
||||||
text: `A post is currently open. Post or discard it before creating a new one.`,
|
text: t`A post is currently open. Post or discard it before creating a new one.`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { execSync } from 'child_process';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import { resolve } from 'path';
|
import { resolve } from 'path';
|
||||||
|
|
||||||
|
import { lingui } from '@lingui/vite-plugin';
|
||||||
import preact from '@preact/preset-vite';
|
import preact from '@preact/preset-vite';
|
||||||
import { uid } from 'uid/single';
|
import { uid } from 'uid/single';
|
||||||
import { defineConfig, loadEnv, splitVendorChunkPlugin } from 'vite';
|
import { defineConfig, loadEnv, splitVendorChunkPlugin } from 'vite';
|
||||||
|
@ -55,8 +56,11 @@ export default defineConfig({
|
||||||
preact({
|
preact({
|
||||||
// Force use Babel instead of ESBuild due to this change: https://github.com/preactjs/preset-vite/pull/114
|
// Force use Babel instead of ESBuild due to this change: https://github.com/preactjs/preset-vite/pull/114
|
||||||
// Else, a bug will happen with importing variables from import.meta.env
|
// Else, a bug will happen with importing variables from import.meta.env
|
||||||
babel: {},
|
babel: {
|
||||||
|
plugins: ['macros'],
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
|
lingui(),
|
||||||
splitVendorChunkPlugin(),
|
splitVendorChunkPlugin(),
|
||||||
removeConsole({
|
removeConsole({
|
||||||
includes: ['log', 'debug', 'info', 'warn', 'error'],
|
includes: ['log', 'debug', 'info', 'warn', 'error'],
|
||||||
|
|
Loading…
Reference in a new issue