Compare commits

...

103 commits

Author SHA1 Message Date
907063c97a Merge branch 'develop' into frontend-rw
Some checks failed
pl-api CI / Test for a successful build (push) Has been cancelled
pl-fe CI / Test and upload artifacts (push) Has been cancelled
pl-fe CI / deploy (push) Has been cancelled
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
2024-10-22 16:07:04 +02:00
775f6b1d2b pl-fe: avoid dangerouslySetInnerHTML
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
2024-10-22 07:44:38 +02:00
04c10bb680 pl-fe: fix typo
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
2024-10-22 07:40:52 +02:00
9b40c46d59 pl-fe: Move emojify to status content parser
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
2024-10-21 23:51:16 +02:00
5b6599f98d pl-fe: WIP Move emojify to status content parser
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
2024-10-21 23:23:48 +02:00
a52dad864b Update en.json
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
2024-10-21 22:47:42 +02:00
c87d0a16a1 pl-fe: Add title to application name link
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
2024-10-21 21:16:11 +02:00
b7a7ea4027 Improve post details styling
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
2024-10-21 21:14:24 +02:00
907a7177de Merge branch 'tassoman-f-ClientApp' into develop 2024-10-21 21:09:07 +02:00
fa38a39dfe pl-fe: introduce wrench reaction button
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
2024-10-21 21:07:20 +02:00
0dd58b714a pl-fe: introduce wrench reaction button
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
2024-10-21 21:04:06 +02:00
c618e1f619 Default to whatever content type is supported
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
2024-10-21 20:39:23 +02:00
47c190cd16 Fix chats
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
2024-10-21 20:35:52 +02:00
ddf464844d Update name/link
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
2024-10-13 16:42:21 +02:00
2dee9f858a Merge branch 'develop' into frontend-rw
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
2024-10-13 16:17:08 +02:00
978e2cccce pl-fe: do not fetch relationship when unauthenticated
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
2024-09-19 11:21:04 +02:00
343a3450a7 pl-fe: do not duplicate read more button
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
2024-09-19 09:18:44 +02:00
fe39a749e4 Merge remote-tracking branch 'mkljczk/develop' into frontend-rw 2024-09-19 09:03:50 +02:00
b35fde5e3a Merge remote-tracking branch 'mkljczk/develop' into frontend-rw 2024-09-19 01:04:20 +02:00
390fe46d23 pl-fe: do not fetch trends if not logged in
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
2024-09-18 21:34:29 +02:00
67243dca8f Merge branch 'develop' into frontend-rw 2024-09-18 21:10:23 +02:00
fb7c3ca4fa Merge remote-tracking branch 'mkljczk/develop' into frontend-rw
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
2024-09-18 18:22:22 +02:00
5e1a39ddc9 modify feature detection
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
2024-09-06 23:31:40 +02:00
d61401dd6d Merge branch 'develop' into frontend-rw 2024-09-06 23:16:56 +02:00
eed9613982 Merge branch 'develop' into frontend-rw
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
2024-09-06 21:27:25 +02:00
9a0de1e4bb Merge branch 'fork' into frontend-rw
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
2024-08-24 12:55:12 +02:00
9dbdff28df cleanup
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
2024-08-24 12:44:03 +02:00
9c4115c217 Merge branch 'fork' into frontend-rw
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
2024-08-24 01:02:41 +02:00
36f9a6df21 lint
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
2024-08-20 15:41:29 +02:00
e745c8557b Merge branch 'fork' into frontend-rw 2024-08-20 15:17:10 +02:00
ea94f12cdf Merge branch 'fork' into frontend-rw
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
2024-08-20 14:41:42 +02:00
d01c3a1eea Merge branch 'fork' into frontend-rw
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
2024-07-02 19:03:11 +02:00
a16fd2f9d4 ESLint fixes?
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
2024-07-01 20:18:26 +02:00
3284a1e2db Merge branch 'fork' into frontend-rw
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
2024-07-01 19:34:08 +02:00
b0d8f22195 Move translation buttons to action bar if auto-translate is enabled
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
2024-07-01 19:10:36 +02:00
cdba672a34 Change profile dropdown placement
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
2024-07-01 00:59:29 +02:00
b52a57fc1c Merge branch 'fork' into frontend-rw
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
2024-06-30 12:05:53 +02:00
5e6f1f00d8 Merge branch 'fork' into frontend-rw 2024-06-02 16:49:41 +02:00
c87a14d72d Fixes
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
2024-06-02 16:43:20 +02:00
874f5998fa Merge branch 'fork' into frontend-rw
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
2024-06-02 15:07:29 +02:00
becacba86a Merge branch 'fork' into frontend-rw
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
2024-05-04 17:28:40 +02:00
864c999373 Mostly port drafts
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
2024-04-03 00:33:14 +02:00
312931876e Merge branch 'drafts' into HEAD
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
2024-04-03 00:05:16 +02:00
a69a539330 Merge remote-tracking branch 'origin/main' into HEAD
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
2024-04-03 00:03:03 +02:00
14505931b5 Merge remote-tracking branch 'origin/bookmark-folders' into HEAD
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
2024-03-23 16:01:21 +01:00
dc4b9dd1b3 Update Lexical
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
2024-03-22 18:00:05 +01:00
777b493aab Merge remote-tracking branch 'origin/main' into HEAD
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
2024-03-22 17:37:37 +01:00
cdbfbf95ae Merge remote-tracking branch 'origin/main' into HEAD
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
2024-01-12 15:03:34 +01:00
6d74d7c573 Don't 'deduplicate' emoji reactions
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
2023-12-29 23:37:21 +01:00
9ee5472bf2 Deduplicate notifications/reposts info
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
2023-12-29 23:21:06 +01:00
2d45d3598a Merge remote-tracking branch 'origin/main' into HEAD
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
2023-12-28 23:44:16 +01:00
e6bff18ec1 Update pl.json
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
2023-11-23 00:05:01 +01:00
7ca39764f1 Fix mentioning bug
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
2023-11-23 00:04:13 +01:00
c5e4bd9f93 Display domain resolve state
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
2023-11-14 13:50:13 +01:00
b5906e919b Domain management
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
2023-11-14 01:05:07 +01:00
0def7e62d5 Merge remote-tracking branch 'pch/frontend-rw' into frontend-rw 2023-11-13 12:09:56 +01:00
ca262bcbb1 Merge remote-tracking branch 'origin/main' into frontend-rw
Signed-off-by: Marcin Mikołajczak <git@mkljczk.pl>
2023-11-08 22:25:04 +01:00
00ea9e3cdf wip
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
2023-11-01 22:51:08 +01:00
ded94fda08 Merge remote-tracking branch 'soapbox/main' into frontend-rw
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
2023-10-31 20:23:54 +01:00
1d406a0ef0 Merge remote-tracking branch 'soapbox/main' into frontend-rw
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
2023-10-28 11:59:59 +02:00
67c10cd4d2 WIP multitenancy, autotranslation
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
2023-10-27 00:22:56 +02:00
75dc7c704f Fix auto translations
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
2023-10-21 23:12:30 +02:00
583ed1215c Translations: Allow to select known languages
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
2023-10-18 22:36:26 +02:00
68292e72b7 Rename application ID
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
2023-10-17 23:09:24 +02:00
8fff46e0e3 Improvements, don't show translations with no difference to original
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
2023-10-17 23:04:23 +02:00
9a073e6872 Merge remote-tracking branch 'soapbox/main' into frontend-rw 2023-10-17 15:19:00 +02:00
bece55207a Merge remote-tracking branch 'soapbox/main' into HEAD
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
2023-10-15 23:37:02 +02:00
705b4a29bc Improve UI, open compose modals in separate window
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
2023-10-12 23:43:52 +02:00
1455e65c1c Auto translate
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
2023-10-11 23:12:40 +02:00
c166b7a5fb Improve translations import
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
2023-10-10 18:04:26 +02:00
f8c54ab387 Fix translated posts columns width
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
2023-10-09 22:33:42 +02:00
f24f99426b Fetch translations with posts, preserve fetched translations
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
2023-10-09 21:59:00 +02:00
43057034eb Merge remote-tracking branch 'soapbox/main' into frontend-rw
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
2023-10-03 22:38:31 +02:00
4b8d7a2378 Improve messages
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
2023-10-03 20:11:37 +02:00
02e299907a Add border to compose form
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
2023-10-03 20:02:15 +02:00
e15979c9ab Add bottom margin to hr
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
2023-10-03 18:29:38 +02:00
aed4d5a578 Display translated text next to original text
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
2023-10-03 18:29:19 +02:00
48becac6be Fix remark export
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
2023-09-24 18:25:41 +02:00
bcdd41f1d7 Merge remote-tracking branch 'soapbox/main' into frontend-rw
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
2023-09-21 23:12:04 +02:00
6bd55a99fb Do not clip statuses in the middle of a paragraph
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
2023-09-20 21:50:38 +02:00
00712185e8 StatusContent: Set collapsed max height to 640px
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
2023-09-20 13:58:58 +02:00
f4368c8d8d Set wysiwyg editor to enabled by default
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
2023-08-22 23:58:03 +02:00
17472cc54c Merge branch 'lexical' into frontend-rw 2023-08-22 23:57:07 +02:00
60be98dad0 Feature gate archive importing
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
2023-08-18 23:45:24 +02:00
1ed4918138 Data importing
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
2023-08-18 20:24:24 +02:00
3d9fb46ccc Merge remote-tracking branch 'soapbox/lexical' into frontend-rw 2023-08-07 21:55:44 +02:00
62e3ef06f8 Move subject field
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
2023-08-05 15:20:29 +02:00
e61ef6dcbe Merge remote-tracking branch 'soapbox/lexical' into frontend-rw
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
2023-08-04 23:00:38 +02:00
51a3f051f3 Merge branch 'lexical' into frontend-rw
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
2023-08-01 23:22:13 +02:00
74f903d62c Revert to subjects
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
2023-07-29 00:29:07 +02:00
34dfe7f22a Merge branch 'lexical' into frontend-rw 2023-07-28 23:26:55 +02:00
7e66b66a95 Display character counters coditionally
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
2023-07-27 23:48:42 +02:00
e7b1eff152 Merge branch 'lexical' into frontend-rw
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
2023-07-27 23:35:47 +02:00
2be52bc207 Merge remote-tracking branch 'soapbox/develop' into frontend-rw 2023-07-27 23:15:58 +02:00
4c9201e8b4 Improve compose page styles, propagate compose toast to other tabs
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
2023-07-26 00:53:22 +02:00
b820781eff Merge remote-tracking branch 'soapbox/develop' into frontend-rw
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
2023-07-25 21:20:14 +02:00
1bcd2b93f1 Add compose page
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
2023-07-24 00:24:48 +02:00
36ffe2fa51 Merge remote-tracking branch 'soapbox/develop' into frontend-rw 2023-07-19 23:56:16 +02:00
7d9eee7a90 Remove links from footer
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
2023-07-15 23:03:21 +02:00
57bc0a9144 Modify directory account card styles
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
2023-07-15 22:58:09 +02:00
48b39abc0c Improve account description overflow handling
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
2023-07-14 23:19:21 +02:00
0a2b07bea0 Hide chats from UI
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
2023-07-14 23:07:49 +02:00
a530d3dfb2 Big Buffet readwrite fork
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
2023-07-12 20:33:55 +02:00
118 changed files with 913 additions and 822 deletions

View file

@ -9,13 +9,38 @@ import { coerceObject, datetimeSchema, filteredArray } from './utils';
const filterBadges = (tags?: string[]) =>
tags?.filter(tag => tag.startsWith('badge:')).map(tag => v.parse(roleSchema, { id: tag, name: tag.replace(/^badge:/, '') }));
const getDomainFromURL = (account: any): string => {
try {
const url = account.url;
return new URL(url).host;
} catch {
return '';
}
};
const guessFqn = (account: any): string => {
const acct = account.acct;
const [user, domain] = acct.split('@');
if (domain) {
return acct;
} else {
return [user, getDomainFromURL(account)].join('@');
}
};
const preprocessAccount = v.transform((account: any) => {
if (!account?.acct) return null;
const username = account.username || account.acct.split('@')[0];
const fqn = account.fqn || guessFqn(account);
const domain = fqn.split('@')[1] || '';
return {
username,
fqn,
domain,
avatar_static: account.avatar_static || account.avatar,
header_static: account.header_static || account.header,
local: typeof account.pleroma?.is_local === 'boolean' ? account.pleroma.is_local : account.acct.split('@')[1] === undefined,
@ -73,7 +98,7 @@ const baseAccountSchema = v.object({
acct: v.fallback(v.string(), ''),
url: v.pipe(v.string(), v.url()),
display_name: v.fallback(v.string(), ''),
note: v.fallback(v.string(), ''),
note: v.fallback(v.pipe(v.string(), v.transform(note => note === '<p></p>' ? '' : note)), ''),
avatar: v.fallback(v.string(), ''),
avatar_static: v.fallback(v.pipe(v.string(), v.url()), ''),
header: v.fallback(v.pipe(v.string(), v.url()), ''),

View file

@ -1,7 +1,7 @@
{
"name": "pl-fe",
"name": "BigBuffet",
"description": "Web-based federated social media client, a fork of Soapbox.",
"keywords": ["fediverse"],
"website": "https://github.com/mkljczk/pl-fe",
"website": "https://forge.pch.net/BigBuffet/frontend-rw",
"stack": "container"
}

View file

@ -1,13 +1,13 @@
{
"name": "pl-fe",
"displayName": "pl-fe",
"displayName": "BigBuffet",
"version": "0.0.1",
"type": "module",
"description": "Web-based federated social media client, a fork of Soapbox",
"homepage": "https://github.com/mkljczk/pl-fe",
"homepage": "https://bigbuff.et",
"repository": {
"type": "git",
"url": "https://github.com/mkljczk/pl-fe"
"url": "https://forge.pch.net/BigBuffet/frontend-rw"
},
"keywords": [
"fediverse",

View file

@ -95,9 +95,7 @@
"compose_form.sensitive.marked": "Media is marked as sensitive",
"compose_form.sensitive.unmarked": "Media is not marked as sensitive",
"compose_form.spoiler.marked": "Text is hidden behind warning",
"compose_form.spoiler.unmarked": "Text is not hidden",
"compose_form.spoiler_placeholder": "Write your warning here",
"confirmation_modal.cancel": "Cancel",
"confirmation_modal.cancel": "Cancel",
"confirmations.block.block_and_report": "Block & Report",
"confirmations.block.confirm": "Block",
"confirmations.block.message": "Are you sure you want to block {name}?",
@ -574,8 +572,6 @@
"compose_form.sensitive.marked": "Media is marked as sensitive",
"compose_form.sensitive.unmarked": "Media is not marked as sensitive",
"compose_form.spoiler.marked": "Text is hidden behind warning",
"compose_form.spoiler.unmarked": "Text is not hidden",
"compose_form.spoiler_placeholder": "Write your warning here",
"confirmation_modal.cancel": "Cancel",
"confirmations.block.block_and_report": "Block & Report",
"confirmations.block.confirm": "Block",

View file

@ -5,6 +5,7 @@ import { getClient } from 'pl-fe/api';
import { isNativeEmoji } from 'pl-fe/features/emoji';
import emojiSearch from 'pl-fe/features/emoji/search';
import { Language } from 'pl-fe/features/preferences';
import { userTouching } from 'pl-fe/is-mobile';
import { selectAccount, selectOwnAccount, makeGetAccount } from 'pl-fe/selectors';
import { tagHistory } from 'pl-fe/settings';
import { useModalsStore } from 'pl-fe/stores/modals';
@ -176,6 +177,9 @@ const replyCompose = (
rebloggedBy?: ComposeReplyAction['rebloggedBy'],
) =>
(dispatch: AppDispatch, getState: () => RootState) => {
if (!userTouching.matches) {
return window.open(`/compose?in_reply_to=${status.id}`, 'targetWindow', 'height=500,width=700');
}
const state = getState();
const client = getClient(state);
const { createStatusExplicitAddressing: explicitAddressing } = client.features;
@ -213,6 +217,9 @@ interface ComposeQuoteAction {
const quoteCompose = (status: ComposeQuoteAction['status']) =>
(dispatch: AppDispatch, getState: () => RootState) => {
if (!userTouching.matches) {
return window.open(`/compose?quote=${status.id}`, 'targetWindow', 'height=500,width=700');
}
const state = getState();
const { createStatusExplicitAddressing: explicitAddressing } = state.auth.client.features;
@ -299,7 +306,7 @@ const directComposeById = (accountId: string) =>
useModalsStore.getState().openModal('COMPOSE');
};
const handleComposeSubmit = (dispatch: AppDispatch, getState: () => RootState, composeId: string, data: BaseStatus | ScheduledStatus, status: string, edit?: boolean) => {
const handleComposeSubmit = (dispatch: AppDispatch, getState: () => RootState, composeId: string, data: BaseStatus | ScheduledStatus, status: string, edit?: boolean, propagate?: boolean) => {
if (!dispatch || !getState) return;
const state = getState();
@ -308,18 +315,24 @@ const handleComposeSubmit = (dispatch: AppDispatch, getState: () => RootState, c
const draftId = getState().compose.get(composeId)!.draft_id;
dispatch(submitComposeSuccess(composeId, data, accountUrl, draftId));
let toastMessage;
let toastOpts;
if (data.scheduled_at === null) {
dispatch(insertIntoTagHistory(composeId, data.tags || [], status));
toast.success(edit ? messages.editSuccess : messages.success, {
toastMessage = edit ? messages.editSuccess : messages.success;
toastOpts = {
actionLabel: messages.view,
actionLink: `/@${data.account.acct}/posts/${data.id}`,
});
};
} else {
toast.success(messages.scheduledSuccess, {
toastMessage = messages.scheduledSuccess;
toastOpts = {
actionLabel: messages.view,
actionLink: '/scheduled_statuses',
});
};
}
if (propagate) toast.propagate('success', toastMessage, toastOpts);
else toast.success(toastMessage, toastOpts);
};
const needsDescriptions = (state: RootState, composeId: string) => {
@ -343,11 +356,13 @@ const validateSchedule = (state: RootState, composeId: string) => {
interface SubmitComposeOpts {
history?: History;
force?: boolean;
onSubmit?: () => void;
propagate?: boolean;
}
const submitCompose = (composeId: string, opts: SubmitComposeOpts = {}) =>
async (dispatch: AppDispatch, getState: () => RootState) => {
const { history, force = false } = opts;
const { history, force = false, onSubmit, propagate } = opts;
if (!isLoggedIn(getState)) return;
const state = getState();
@ -359,6 +374,7 @@ const submitCompose = (composeId: string, opts: SubmitComposeOpts = {}) =>
const statusId = compose.id;
let to = compose.to;
if (!validateSchedule(state, composeId)) {
toast.error(messages.scheduleError);
return;
@ -443,7 +459,8 @@ const submitCompose = (composeId: string, opts: SubmitComposeOpts = {}) =>
if (!statusId && data.scheduled_at === null && data.visibility === 'direct' && getState().conversations.mounted <= 0 && history) {
history.push('/conversations');
}
handleComposeSubmit(dispatch, getState, composeId, data, status, !!statusId);
handleComposeSubmit(dispatch, getState, composeId, data, status, !!statusId, propagate);
if (onSubmit) onSubmit();
}).catch((error) => {
dispatch(submitComposeFail(composeId, error));
});

View file

@ -1,3 +1,4 @@
import { serialize } from 'object-to-formdata';
import { defineMessages } from 'react-intl';
import toast from 'pl-fe/toast';
@ -18,6 +19,10 @@ const IMPORT_MUTES_REQUEST = 'IMPORT_MUTES_REQUEST' as const;
const IMPORT_MUTES_SUCCESS = 'IMPORT_MUTES_SUCCESS' as const;
const IMPORT_MUTES_FAIL = 'IMPORT_MUTES_FAIL' as const;
const IMPORT_ARCHIVE_REQUEST = 'IMPORT_ARCHIVE_REQUEST';
const IMPORT_ARCHIVE_SUCCESS = 'IMPORT_ARCHIVE_SUCCESS';
const IMPORT_ARCHIVE_FAIL = 'IMPORT_ARCHIVE_FAIL';
type ImportDataActions = {
type: typeof IMPORT_FOLLOWS_REQUEST
| typeof IMPORT_FOLLOWS_SUCCESS
@ -27,7 +32,10 @@ type ImportDataActions = {
| typeof IMPORT_BLOCKS_FAIL
| typeof IMPORT_MUTES_REQUEST
| typeof IMPORT_MUTES_SUCCESS
| typeof IMPORT_MUTES_FAIL;
| typeof IMPORT_MUTES_FAIL
| typeof IMPORT_ARCHIVE_REQUEST
| typeof IMPORT_ARCHIVE_SUCCESS
| typeof IMPORT_ARCHIVE_FAIL;
error?: any;
response?: string;
}
@ -36,6 +44,7 @@ const messages = defineMessages({
blocksSuccess: { id: 'import_data.success.blocks', defaultMessage: 'Blocks imported successfully' },
followersSuccess: { id: 'import_data.success.followers', defaultMessage: 'Followers imported successfully' },
mutesSuccess: { id: 'import_data.success.mutes', defaultMessage: 'Mutes imported successfully' },
archiveSuccess: { id: 'import_data.success.archive', defaultMessage: 'Archive imported successfully' },
});
const importFollows = (list: File | string, overwrite?: boolean) =>
@ -71,6 +80,23 @@ const importMutes = (list: File | string) =>
});
};
const importArchive = (file: File) =>
(dispatch: React.Dispatch<ImportDataActions>, getState: () => RootState) => {
dispatch({ type: IMPORT_ARCHIVE_REQUEST });
const form = serialize({ file, keep_unlisted: true }, { indices: true });
return getClient(getState).request('/api/pleroma/archive_import', {
method: 'POST',
body: form,
contentType: '',
})
.then(response => {
toast.success(messages.archiveSuccess);
dispatch({ type: IMPORT_ARCHIVE_SUCCESS, response: response.json });
}).catch(error => {
dispatch({ type: IMPORT_ARCHIVE_FAIL, error });
});
};
export {
IMPORT_FOLLOWS_REQUEST,
IMPORT_FOLLOWS_SUCCESS,
@ -81,7 +107,11 @@ export {
IMPORT_MUTES_REQUEST,
IMPORT_MUTES_SUCCESS,
IMPORT_MUTES_FAIL,
IMPORT_ARCHIVE_REQUEST,
IMPORT_ARCHIVE_SUCCESS,
IMPORT_ARCHIVE_FAIL,
importFollows,
importBlocks,
importMutes,
importArchive,
};

View file

@ -3,6 +3,7 @@ import * as v from 'valibot';
import { Entities } from 'pl-fe/entity-store/entities';
import { useEntity } from 'pl-fe/entity-store/hooks/useEntity';
import { useClient } from 'pl-fe/hooks/useClient';
import { useLoggedIn } from 'pl-fe/hooks/useLoggedIn';
import type { Relationship } from 'pl-api';
@ -12,13 +13,14 @@ interface UseRelationshipOpts {
const useRelationship = (accountId: string | undefined, opts: UseRelationshipOpts = {}) => {
const client = useClient();
const { isLoggedIn } = useLoggedIn();
const { enabled = false } = opts;
const { entity: relationship, ...result } = useEntity<Relationship>(
[Entities.RELATIONSHIPS, accountId!],
() => client.accounts.getRelationships([accountId!]),
{
enabled: enabled && !!accountId,
enabled: enabled && isLoggedIn && !!accountId,
schema: v.pipe(v.any(), v.transform(arr => arr[0])),
},
);

View file

@ -1,14 +1,13 @@
import { useMutation, useQuery } from '@tanstack/react-query';
import {
adminAnnouncementSchema,
type AdminAnnouncement as BaseAdminAnnouncement,
type AdminAnnouncement,
type AdminCreateAnnouncementParams,
type AdminUpdateAnnouncementParams,
} from 'pl-api';
import * as v from 'valibot';
import { useClient } from 'pl-fe/hooks/useClient';
import { normalizeAnnouncement, AdminAnnouncement } from 'pl-fe/normalizers/announcement';
import { queryClient } from 'pl-fe/queries/client';
import { useAnnouncements as useUserAnnouncements } from '../announcements/useAnnouncements';
@ -20,7 +19,7 @@ const useAnnouncements = () => {
const getAnnouncements = async () => {
const data = await client.admin.announcements.getAnnouncements();
return data.items.map(normalizeAnnouncement<BaseAdminAnnouncement>);
return data.items;
};
const result = useQuery<ReadonlyArray<AdminAnnouncement>>({

View file

@ -1,9 +1,8 @@
import { useMutation, useQuery } from '@tanstack/react-query';
import { announcementReactionSchema, type AnnouncementReaction } from 'pl-api';
import { announcementReactionSchema, type AnnouncementReaction, type Announcement } from 'pl-api';
import * as v from 'valibot';
import { useClient } from 'pl-fe/hooks/useClient';
import { type Announcement, normalizeAnnouncement } from 'pl-fe/normalizers/announcement';
import { queryClient } from 'pl-fe/queries/client';
const updateReaction = (reaction: AnnouncementReaction, count: number, me?: boolean, overwrite?: boolean) => v.parse(announcementReactionSchema, {
@ -25,14 +24,9 @@ const updateReactions = (reactions: AnnouncementReaction[], name: string, count:
const useAnnouncements = () => {
const client = useClient();
const getAnnouncements = async () => {
const data = await client.announcements.getAnnouncements();
return data.map(normalizeAnnouncement);
};
const { data, ...result } = useQuery<ReadonlyArray<Announcement>>({
queryKey: ['announcements'],
queryFn: getAnnouncements,
queryFn: () => client.announcements.getAnnouncements(),
placeholderData: [],
});

View file

@ -160,7 +160,7 @@ const AccountHoverCard: React.FC<IAccountHoverCard> = ({ visible = true }) => {
size='sm'
className='mr-2 rtl:ml-2 rtl:mr-0 [&_br]:hidden [&_p:first-child]:inline [&_p:first-child]:truncate [&_p]:hidden'
>
<ParsedContent html={account.note_emojified} />
<ParsedContent html={account.note} emojis={account.emojis} />
</Text>
)}
</Stack>

View file

@ -11,6 +11,7 @@ import IconButton from 'pl-fe/components/ui/icon-button';
import Stack from 'pl-fe/components/ui/stack';
import Text from 'pl-fe/components/ui/text';
import VerificationBadge from 'pl-fe/components/verification-badge';
import Emojify from 'pl-fe/features/emoji/emojify';
import ActionButton from 'pl-fe/features/ui/components/action-button';
import { useAppSelector } from 'pl-fe/hooks/useAppSelector';
import { getAcct } from 'pl-fe/utils/accounts';
@ -219,8 +220,9 @@ const Account = ({
size='sm'
weight='semibold'
truncate
dangerouslySetInnerHTML={{ __html: account.display_name_html }}
/>
>
<Emojify text={account.display_name} emojis={account.emojis} />
</Text>
{account.verified && <VerificationBadge />}
@ -281,8 +283,9 @@ const Account = ({
size='sm'
weight='semibold'
truncate
dangerouslySetInnerHTML={{ __html: account.display_name_html }}
/>
>
<Emojify text={account.display_name} emojis={account.emojis} />
</Text>
{account.verified && <VerificationBadge />}
@ -356,7 +359,7 @@ const Account = ({
truncate
size='sm'
>
<ParsedContent html={account.note_emojified} />
<ParsedContent html={account.note} emojis={account.emojis} />
</Text>
)}
</Stack>

View file

@ -3,8 +3,9 @@ import { useHistory } from 'react-router-dom';
import { getTextDirection } from 'pl-fe/utils/rtl';
import type { Mention as MentionEntity } from 'pl-api';
import type { Announcement } from 'pl-fe/normalizers/announcement';
import { ParsedContent } from '../parsed-content';
import type { Announcement, Mention as MentionEntity } from 'pl-api';
interface IAnnouncementContent {
announcement: Announcement;
@ -83,8 +84,9 @@ const AnnouncementContent: React.FC<IAnnouncementContent> = ({ announcement }) =
dir={direction}
className='text-sm ltr:ml-0 rtl:mr-0'
ref={node}
dangerouslySetInnerHTML={{ __html: announcement.contentHtml }}
/>
>
<ParsedContent html={announcement.content} emojis={announcement.emojis} />
</div>
);
};

View file

@ -10,8 +10,7 @@ import AnnouncementContent from './announcement-content';
import ReactionsBar from './reactions-bar';
import type { Map as ImmutableMap } from 'immutable';
import type { CustomEmoji } from 'pl-api';
import type { Announcement as AnnouncementEntity } from 'pl-fe/normalizers/announcement';
import type { Announcement as AnnouncementEntity, CustomEmoji } from 'pl-api';
interface IAnnouncement {
announcement: AnnouncementEntity;

View file

@ -8,6 +8,7 @@ import HStack from 'pl-fe/components/ui/hstack';
import Stack from 'pl-fe/components/ui/stack';
import Text from 'pl-fe/components/ui/text';
import VerificationBadge from 'pl-fe/components/verification-badge';
import Emojify from 'pl-fe/features/emoji/emojify';
import EventActionButton from 'pl-fe/features/event/components/event-action-button';
import EventDate from 'pl-fe/features/event/components/event-date';
import { useAppSelector } from 'pl-fe/hooks/useAppSelector';
@ -71,7 +72,9 @@ const EventPreview: React.FC<IEventPreview> = ({ status, className, hideAction,
<HStack alignItems='center' space={2}>
<Icon src={require('@tabler/icons/outline/user.svg')} />
<HStack space={1} alignItems='center' grow>
<span dangerouslySetInnerHTML={{ __html: account.display_name_html }} />
<span>
<Emojify text={account.display_name} emojis={account.emojis} />
</span>
{account.verified && <VerificationBadge />}
</HStack>
</HStack>

View file

@ -3,6 +3,7 @@ import React from 'react';
import HStack from 'pl-fe/components/ui/hstack';
import Stack from 'pl-fe/components/ui/stack';
import Text from 'pl-fe/components/ui/text';
import Emojify from 'pl-fe/features/emoji/emojify';
import GroupHeaderImage from 'pl-fe/features/group/components/group-header-image';
import GroupMemberCount from 'pl-fe/features/group/components/group-member-count';
import GroupPrivacy from 'pl-fe/features/group/components/group-privacy';
@ -37,7 +38,9 @@ const GroupCard: React.FC<IGroupCard> = ({ group }) => (
{/* Group Info */}
<Stack alignItems='center' justifyContent='end' grow className='basis-1/2 py-4' space={0.5}>
<HStack alignItems='center' space={1.5}>
<Text size='lg' weight='bold' dangerouslySetInnerHTML={{ __html: group.display_name_html }} />
<Text size='lg' weight='bold'>
<Emojify text={group.display_name} emojis={group.emojis} />
</Text>
</HStack>
<HStack className='text-gray-700 dark:text-gray-600' space={2} wrap>

View file

@ -8,6 +8,7 @@ import HStack from 'pl-fe/components/ui/hstack';
import Popover from 'pl-fe/components/ui/popover';
import Stack from 'pl-fe/components/ui/stack';
import Text from 'pl-fe/components/ui/text';
import Emojify from 'pl-fe/features/emoji/emojify';
import GroupMemberCount from 'pl-fe/features/group/components/group-member-count';
import GroupPrivacy from 'pl-fe/features/group/components/group-privacy';
@ -71,7 +72,9 @@ const GroupPopover = (props: IGroupPopoverContainer) => {
{/* Group Info */}
<Stack alignItems='center' justifyContent='end' grow className='basis-1/2 py-4' space={0.5}>
<Text size='lg' weight='bold' dangerouslySetInnerHTML={{ __html: group.display_name_html }} />
<Text size='lg' weight='bold'>
<Emojify text={group.display_name} emojis={group.emojis} />
</Text>
<HStack className='text-gray-700 dark:text-gray-600' space={2} wrap>
<GroupPrivacy group={group} />

View file

@ -1,7 +1,6 @@
import React from 'react';
import { Helmet as ReactHelmet } from 'react-helmet-async';
import { useStatContext } from 'pl-fe/contexts/stat-context';
import { useAppSelector } from 'pl-fe/hooks/useAppSelector';
import { useInstance } from 'pl-fe/hooks/useInstance';
import { useSettings } from 'pl-fe/hooks/useSettings';
@ -23,8 +22,7 @@ interface IHelmet {
const Helmet: React.FC<IHelmet> = ({ children }) => {
const instance = useInstance();
const { unreadChatsCount } = useStatContext();
const unreadCount = useAppSelector((state) => getNotifTotals(state) + unreadChatsCount);
const unreadCount = useAppSelector((state) => getNotifTotals(state));
const { demetricator } = useSettings();
const hasUnreadNotifications = React.useMemo(() => !(unreadCount < 1 || demetricator), [unreadCount, demetricator]);

View file

@ -0,0 +1,134 @@
[data-markup] {
@apply whitespace-pre-wrap;
}
[data-markup] h1 {
@apply text-3xl font-semibold;
}
[data-markup] h2 {
@apply text-2xl font-semibold;
}
[data-markup] h3 {
@apply text-xl font-black;
}
[data-markup] hr {
@apply mb-4;
}
[data-markup] p {
@apply mb-4 whitespace-pre-wrap;
}
[data-markup] p:last-child,
[data-markup] hr:last-child {
@apply mb-0;
}
[data-markup] a {
@apply text-primary-600 dark:text-accent-blue hover:underline;
}
[data-markup] strong {
@apply font-bold;
}
[data-markup] em {
@apply italic;
}
[data-markup] ul,
[data-markup] ol {
@apply pl-10 mb-4;
}
[data-markup] ul {
@apply list-disc list-outside;
}
[data-markup] ol {
@apply list-decimal list-outside;
}
[data-markup] blockquote {
@apply py-1 pl-4 mb-4 border-l-4 border-solid border-gray-400 text-gray-500 dark:text-gray-400;
}
[data-markup] table {
@apply table-auto w-full bg-gray-200 dark:bg-gray-900 my-4 rounded-md;
}
[data-markup] table th, table td {
@apply text-center px-2;
}
[data-markup] table th {
@apply border-b-2 border-gray-600;
}
[data-markup] code,
[data-markup] pre {
@apply cursor-text font-mono;
}
[data-markup] p > code,
[data-markup] pre {
@apply bg-gray-100 dark:bg-primary-800;
}
/* Inline code */
[data-markup] p > code {
@apply py-0.5 px-1 rounded-sm;
}
/* Code block */
[data-markup] pre {
@apply py-2 px-3 mb-4 leading-6 overflow-x-auto rounded-md break-all;
}
[data-markup] pre:last-child {
@apply mb-0;
}
/* Emojis */
[data-markup] img.emojione {
@apply w-5 h-5 m-0;
}
/* Markdown inline images (Pleroma) */
[data-markup] img:not(.emojione) {
@apply max-h-[500px] mx-auto rounded-sm;
}
/* User setting to underline links */
body.underline-links [data-markup] a {
@apply underline;
}
[data-markup].big-emoji img.emojione {
@apply inline w-9 h-9 p-1;
}
[data-markup] .status-link {
@apply hover:underline text-primary-600 dark:text-accent-blue hover:text-primary-800 dark:hover:text-accent-blue;
}
[data-markup] .invisible {
font-size: 0 !important;
line-height: 0 !important;
display: inline-block;
width: 0;
height: 0;
position: absolute;
}
[data-markup] .invisible img,
[data-markup] .invisible svg {
margin: 0 !important;
border: 0 !important;
padding: 0 !important;
width: 0 !important;
height: 0 !important;
}

View file

@ -289,6 +289,7 @@ const MediaGallery: React.FC<IMediaGallery> = (props) => {
height,
visible,
} = props;
const [width, setWidth] = useState<number>(defaultWidth);
const node = useRef<HTMLDivElement>(null);

View file

@ -3,11 +3,14 @@ import DOMPurify from 'isomorphic-dompurify';
import React, { useMemo } from 'react';
import { Link } from 'react-router-dom';
import Emojify from 'pl-fe/features/emoji/emojify';
import { makeEmojiMap } from 'pl-fe/utils/normalizers';
import HashtagLink from './hashtag-link';
import HoverAccountWrapper from './hover-account-wrapper';
import StatusMention from './status-mention';
import type { Mention } from 'pl-api';
import type { CustomEmoji, Mention } from 'pl-api';
const nodesToText = (nodes: Array<DOMNode>): string =>
nodes.map(node => node.type === 'text' ? node.data : node.type === 'tag' ? nodesToText(node.children as Array<DOMNode>) : '').join('');
@ -19,14 +22,18 @@ interface IParsedContent {
mentions?: Array<Mention>;
/** Whether it's a status which has a quote. */
hasQuote?: boolean;
/** Related custom emojis. */
emojis?: Array<CustomEmoji>;
}
const ParsedContent: React.FC<IParsedContent> = (({ html, mentions, hasQuote }) => {
const ParsedContent: React.FC<IParsedContent> = (({ html, mentions, hasQuote, emojis }) => {
return useMemo(() => {
if (html.length === 0) {
return null;
}
const emojiMap = emojis ? makeEmojiMap(emojis) : undefined;
const selectors: Array<string> = [];
// Explicit mentions
@ -99,6 +106,14 @@ const ParsedContent: React.FC<IParsedContent> = (({ html, mentions, hasQuote })
return fallback;
}
},
transform(reactNode) {
if (typeof reactNode === 'string') {
return <Emojify text={reactNode} emojis={emojiMap} />;
}
return reactNode as JSX.Element;
},
};
return parse(DOMPurify.sanitize(html, { ADD_ATTR: ['target'], USE_PROFILES: { html: true } }), options);

View file

@ -12,7 +12,7 @@ import { useAppDispatch } from 'pl-fe/hooks/useAppDispatch';
import RelativeTimestamp from '../relative-timestamp';
import type { Selected } from './poll';
import type { Poll } from 'pl-fe/normalizers/poll';
import type { Poll } from 'pl-api';
const messages = defineMessages({
closed: { id: 'poll.closed', defaultMessage: 'Closed' },

View file

@ -7,7 +7,9 @@ import HStack from 'pl-fe/components/ui/hstack';
import Icon from 'pl-fe/components/ui/icon';
import Text from 'pl-fe/components/ui/text';
import type { Poll } from 'pl-fe/normalizers/poll';
import { ParsedContent } from '../parsed-content';
import type { Poll } from 'pl-api';
const messages = defineMessages({
voted: { id: 'poll.voted', defaultMessage: 'You voted for this answer' },
@ -65,8 +67,9 @@ const PollOptionText: React.FC<IPollOptionText> = ({ poll, option, index, active
theme='inherit'
weight='medium'
align='center'
dangerouslySetInnerHTML={{ __html: option.title_emojified }}
/>
>
<ParsedContent html={option.title} emojis={poll.emojis} />
</Text>
</div>
</div>
@ -133,9 +136,10 @@ const PollOption: React.FC<IPollOption> = (props): JSX.Element | null => {
<Text
theme='inherit'
weight='medium'
dangerouslySetInnerHTML={{ __html: (language && option.title_map_emojified) && option.title_map_emojified[language] || option.title_emojified }}
className='relative'
/>
>
<ParsedContent html={(language && option.title_map) && option.title_map[language] || option.title} emojis={poll.emojis} />
</Text>
</div>
<HStack space={2} alignItems='center' className='relative'>

View file

@ -3,8 +3,6 @@ import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import Icon from 'pl-fe/components/ui/icon';
import Stack from 'pl-fe/components/ui/stack';
import { useStatContext } from 'pl-fe/contexts/stat-context';
import Search from 'pl-fe/features/search/components/search';
import ComposeButton from 'pl-fe/features/ui/components/compose-button';
import ProfileDropdown from 'pl-fe/features/ui/components/profile-dropdown';
import { useAppSelector } from 'pl-fe/hooks/useAppSelector';
@ -36,7 +34,6 @@ const messages = defineMessages({
/** Desktop sidebar with links to different views in the app. */
const SidebarNavigation = () => {
const intl = useIntl();
const { unreadChatsCount } = useStatContext();
const instance = useInstance();
const features = useFeatures();
@ -163,9 +160,6 @@ const SidebarNavigation = () => {
/>
</ProfileDropdown>
</div>
<div className='block w-full max-w-xs'>
<Search openInRoute autosuggest />
</div>
</Stack>
)}
@ -193,16 +187,6 @@ const SidebarNavigation = () => {
text={<FormattedMessage id='tabs_bar.notifications' defaultMessage='Notifications' />}
/>
{features.chats && (
<SidebarNavigationLink
to='/chats'
icon={require('@tabler/icons/outline/messages.svg')}
count={unreadChatsCount}
countMax={9}
text={<FormattedMessage id='navigation.chats' defaultMessage='Chats' />}
/>
)}
{!features.chats && features.conversations && (
<SidebarNavigationLink
to='/conversations'

View file

@ -5,7 +5,7 @@ import { useHistory, useRouteMatch } from 'react-router-dom';
import { blockAccount } from 'pl-fe/actions/accounts';
import { directCompose, mentionCompose, quoteCompose, replyCompose } from 'pl-fe/actions/compose';
import { emojiReact } from 'pl-fe/actions/emoji-reacts';
import { emojiReact, unEmojiReact } from 'pl-fe/actions/emoji-reacts';
import { editEvent } from 'pl-fe/actions/events';
import { toggleBookmark, toggleDislike, toggleFavourite, togglePin, toggleReblog } from 'pl-fe/actions/interactions';
import { deleteStatusModal, toggleStatusSensitivityModal } from 'pl-fe/actions/moderation';
@ -104,6 +104,7 @@ const messages = defineMessages({
unmuteConversation: { id: 'status.unmute_conversation', defaultMessage: 'Unmute conversation' },
unpin: { id: 'status.unpin', defaultMessage: 'Unpin from profile' },
viewReactions: { id: 'status.view_reactions', defaultMessage: 'View reactions' },
wrench: { id: 'status.wrench', defaultMessage: 'Wrench reaction' },
addKnownLanguage: { id: 'status.add_known_language', defaultMessage: 'Do not auto-translate posts in {language}.' },
translate: { id: 'status.translate', defaultMessage: 'Translate' },
hideTranslation: { id: 'status.hide_translation', defaultMessage: 'Hide translation' },
@ -143,7 +144,9 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
const { groupRelationship } = useGroupRelationship(status.group_id || undefined);
const features = useFeatures();
const instance = useInstance();
const { autoTranslate, boostModal, deleteModal, knownLanguages } = useSettings();
const { autoTranslate, boostModal, deleteModal, knownLanguages, showWrenchButton } = useSettings();
const wrenches = showWrenchButton && status.emoji_reactions.find(emoji => emoji.name === '🔧') || undefined;
const { translationLanguages } = useTranslationLanguages();
@ -153,7 +156,7 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
allow_unauthenticated: allowUnauthenticated,
} = instance.pleroma.metadata.translation;
const renderTranslate = (me || allowUnauthenticated) && (allowRemote || status.account.local) && ['public', 'unlisted'].includes(status.visibility) && status.contentHtml.length > 0 && status.language !== null && !knownLanguages.includes(status.language);
const renderTranslate = (me || allowUnauthenticated) && (allowRemote || status.account.local) && ['public', 'unlisted'].includes(status.visibility) && status.content.length > 0 && status.language !== null && !knownLanguages.includes(status.language);
const supportsLanguages = (translationLanguages[status.language!]?.includes(intl.locale));
return autoTranslate && features.translations && renderTranslate && supportsLanguages;
@ -211,10 +214,24 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
}
};
const handleWrenchClick: React.EventHandler<React.MouseEvent> = (e) => {
if (!me) {
onOpenUnauthorizedModal('DISLIKE');
} else if (wrenches?.me) {
dispatch(unEmojiReact(status, '🔧'));
} else {
dispatch(emojiReact(status, '🔧'));
}
};
const handleDislikeLongPress = status.dislikes_count ? () => {
openModal('DISLIKES', { statusId: status.id });
} : undefined;
const handleWrenchLongPress = wrenches?.count ? () => {
openModal('REACTIONS', { statusId: status.id, reaction: wrenches.name });
} : undefined;
const handlePickEmoji = (emoji: EmojiType) => {
dispatch(emojiReact(status, emoji.custom ? emoji.id : emoji.native, emoji.custom ? emoji.imageUrl : undefined));
};
@ -784,6 +801,20 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
/>
)}
{me && !withLabels && features.emojiReacts && showWrenchButton && (
<StatusActionButton
title={intl.formatMessage(messages.wrench)}
icon={require('@tabler/icons/outline/tool.svg')}
color='accent'
filled
onClick={handleWrenchClick}
onLongPress={handleWrenchLongPress}
active={wrenches?.me}
count={wrenches?.count || undefined}
theme={statusActionButtonTheme}
/>
)}
{me && !withLabels && features.emojiReacts && (
<EmojiPickerDropdown
onPickEmoji={handlePickEmoji}

View file

@ -1,14 +1,11 @@
import clsx from 'clsx';
import React, { useState, useRef, useLayoutEffect, useMemo, useEffect } from 'react';
import React, { useState, useRef, useLayoutEffect } from 'react';
import { FormattedMessage } from 'react-intl';
import { collapseStatusSpoiler, expandStatusSpoiler } from 'pl-fe/actions/statuses';
import Icon from 'pl-fe/components/icon';
import Button from 'pl-fe/components/ui/button';
import Stack from 'pl-fe/components/ui/stack';
import Text from 'pl-fe/components/ui/text';
import { useAppDispatch } from 'pl-fe/hooks/useAppDispatch';
import { useSettings } from 'pl-fe/hooks/useSettings';
import Emojify from 'pl-fe/features/emoji/emojify';
import { onlyEmoji as isOnlyEmoji } from 'pl-fe/utils/rich-content';
import { getTextDirection } from '../utils/rtl';
@ -63,15 +60,12 @@ const StatusContent: React.FC<IStatusContent> = React.memo(({
textSize = 'md',
quote = false,
}) => {
const dispatch = useAppDispatch();
const { displaySpoilers } = useSettings();
const [collapsed, setCollapsed] = useState(false);
const [onlyEmoji, setOnlyEmoji] = useState(false);
const [lineClamp, setLineClamp] = useState(true);
const [isTranslationEqual, setIsTranslationEqual] = useState(false);
const node = useRef<HTMLDivElement>(null);
const spoilerNode = useRef<HTMLSpanElement>(null);
const translationNode = useRef<HTMLDivElement>(null);
const maybeSetCollapsed = (): void => {
if (!node.current) return;
@ -93,37 +87,26 @@ const StatusContent: React.FC<IStatusContent> = React.memo(({
}
};
const toggleExpanded: React.MouseEventHandler<HTMLButtonElement> = (e) => {
e.preventDefault();
e.stopPropagation();
if (expanded) dispatch(collapseStatusSpoiler(status.id));
else dispatch(expandStatusSpoiler(status.id));
};
useLayoutEffect(() => {
maybeSetCollapsed();
maybeSetOnlyEmoji();
});
const parsedHtml = useMemo(
(): string => translatable && status.translation
? status.translation.content!
: (status.contentMapHtml && status.currentLanguage)
? (status.contentMapHtml[status.currentLanguage] || status.contentHtml)
: status.contentHtml,
[status.contentHtml, status.translation, status.currentLanguage],
);
useLayoutEffect(() => {
setIsTranslationEqual(node.current?.innerText.trim().replace(/\s+/g, ' ') === translationNode.current?.innerText.trim().replace(/\s+/g, ' '));
}, [status.translation]);
useEffect(() => {
setLineClamp(!spoilerNode.current || spoilerNode.current.clientHeight >= 96);
}, [spoilerNode.current]);
const content = (status.content_map && status.currentLanguage)
? status.content_map[status.currentLanguage] || status.content
: status.content;
const withSpoiler = status.spoiler_text.length > 0;
const translationContent = translatable && status.translation && !isTranslationEqual ? status.translation.content : null;
const spoilerText = status.spoilerMapHtml && status.currentLanguage
? status.spoilerMapHtml[status.currentLanguage] || status.spoilerHtml
: status.spoilerHtml;
const baseClassName = 'text-gray-900 dark:text-gray-100 break-words text-ellipsis overflow-hidden relative focus:outline-none';
const spoilerText = status.spoiler_text_map && status.currentLanguage
? status.spoiler_text_map[status.currentLanguage] || status.spoiler_text
: status.spoiler_text;
const direction = getTextDirection(status.search_index);
const className = clsx('relative text-ellipsis break-words text-gray-900 focus:outline-none dark:text-gray-100', {
@ -134,42 +117,21 @@ const StatusContent: React.FC<IStatusContent> = React.memo(({
'leading-normal big-emoji': onlyEmoji,
});
const expandable = !displaySpoilers;
const expanded = !withSpoiler || status.expanded || false;
const output = [];
if (spoilerText) {
output.push(
<Text key='spoiler' size='2xl' weight='medium'>
<span
className={clsx({ 'line-clamp-3': !expanded && lineClamp })}
dangerouslySetInnerHTML={{ __html: spoilerText }}
ref={spoilerNode}
/>
{status.content && expandable && (
<Button
className='ml-2 align-middle'
type='button'
theme='muted'
size='xs'
onClick={toggleExpanded}
icon={expanded ? require('@tabler/icons/outline/chevron-up.svg') : require('@tabler/icons/outline/chevron-down.svg')}
>
{expanded
? <FormattedMessage id='status.spoiler.collapse' defaultMessage='Collapse' />
: <FormattedMessage id='status.spoiler.expand' defaultMessage='Expand' />}
</Button>
)}
<span>
<Emojify text={spoilerText} emojis={status.emojis} />
</span>
</Text>,
);
}
if (expandable && !expanded) return <>{output}</>;
if (onClick) {
if (status.content) {
output.push(
if (content) {
const body = (
<Markup
ref={node}
tabIndex={0}
@ -179,14 +141,39 @@ const StatusContent: React.FC<IStatusContent> = React.memo(({
lang={status.language || undefined}
size={textSize}
>
<ParsedContent html={parsedHtml} mentions={status.mentions} hasQuote={!!status.quote_id} />
</Markup>,
<ParsedContent html={content} mentions={status.mentions} hasQuote={!!status.quote_id} emojis={status.emojis} />
</Markup>
);
if (translationContent && !isTranslationEqual) {
output.push(
<div className='flex flex-col gap-2 sm:flex-row'>
<div className='break-word-nested w-full grow'>
{body}
</div>
<hr className='sm:hidden' />
<div className='break-word-nested w-full grow'>
<Markup
ref={translationNode}
tabIndex={0}
className={className}
direction={direction}
lang={status.language || undefined}
size={textSize}
>
<ParsedContent html={translationContent} mentions={status.mentions} hasQuote={!!status.quote_id} />
</Markup>
</div>
</div>,
);
} else {
output.push(body);
}
}
const hasPoll = !!status.poll_id;
if (collapsed) {
if (content && collapsed) {
output.push(<ReadMoreButton onClick={onClick} key='read-more' quote={quote} poll={hasPoll} />);
}
@ -196,8 +183,8 @@ const StatusContent: React.FC<IStatusContent> = React.memo(({
return <Stack space={4} className={clsx({ 'bg-gray-100 dark:bg-primary-800 rounded-md p-4': hasPoll })}>{output}</Stack>;
} else {
if (status.content) {
output.push(
if (content) {
const body = (
<Markup
ref={node}
tabIndex={0}
@ -207,13 +194,40 @@ const StatusContent: React.FC<IStatusContent> = React.memo(({
lang={status.language || undefined}
size={textSize}
>
<ParsedContent html={parsedHtml} mentions={status.mentions} hasQuote={!!status.quote_id} />
</Markup>,
<ParsedContent html={content} mentions={status.mentions} hasQuote={!!status.quote_id} emojis={status.emojis} />
</Markup>
);
}
if (collapsed) {
output.push(<ReadMoreButton onClick={() => {}} key='read-more' quote={quote} />);
if (translationContent && !isTranslationEqual) {
output.push(
<div className='flex flex-col gap-2 sm:flex-row'>
<div className='break-word-nested w-full grow'>
{body}
</div>
<hr className='sm:hidden' />
<div className='break-word-nested w-full grow'>
<Markup
ref={translationNode}
tabIndex={0}
key='translated_content'
className={clsx(baseClassName, {
'leading-normal big-emoji': onlyEmoji,
})}
direction={direction}
size={textSize}
>
<ParsedContent html={translationContent} mentions={status.mentions} hasQuote={!!status.quote_id} />
</Markup>
</div>
</div>,
);
} else {
output.push(body);
}
if (collapsed) {
output.push(<ReadMoreButton onClick={() => {}} key='read-more' quote={quote} />);
}
}
if (status.poll_id) {

View file

@ -17,7 +17,7 @@ const messages = defineMessages({
});
interface IStatusLanguagePicker {
status: Pick<Status, 'id' | 'contentMapHtml' | 'currentLanguage'>;
status: Pick<Status, 'id' | 'content_map' | 'currentLanguage'>;
showLabel?: boolean;
}
@ -25,7 +25,7 @@ const StatusLanguagePicker: React.FC<IStatusLanguagePicker> = ({ status, showLab
const intl = useIntl();
const dispatch = useAppDispatch();
if (!status.contentMapHtml || Object.keys(status.contentMapHtml).length < 2) return null;
if (!status.content_map || Object.keys(status.content_map).length < 2) return null;
const icon = <Icon className='size-4 text-gray-700 dark:text-gray-600' src={require('@tabler/icons/outline/language.svg')} />;
@ -34,7 +34,7 @@ const StatusLanguagePicker: React.FC<IStatusLanguagePicker> = ({ status, showLab
<Text tag='span' theme='muted' size='sm'>&middot;</Text>
<DropdownMenu
items={Object.keys(status.contentMapHtml).map((language) => ({
items={Object.keys(status.content_map).map((language) => ({
text: languages[language as Language] || language,
action: () => dispatch(changeStatusLanguage(status.id, language)),
active: language === status.currentLanguage,

View file

@ -12,6 +12,7 @@ import Icon from 'pl-fe/components/ui/icon';
import Stack from 'pl-fe/components/ui/stack';
import Text from 'pl-fe/components/ui/text';
import AccountContainer from 'pl-fe/containers/account-container';
import Emojify from 'pl-fe/features/emoji/emojify';
import StatusTypeIcon from 'pl-fe/features/status/components/status-type-icon';
import QuotedStatus from 'pl-fe/features/status/containers/quoted-status-container';
import { HotKeys } from 'pl-fe/features/ui/components/hotkeys';
@ -204,23 +205,17 @@ const Status: React.FC<IStatus> = (props) => {
className='hover:underline'
>
<bdi className='truncate'>
<strong
className='text-gray-800 dark:text-gray-200'
dangerouslySetInnerHTML={{
__html: status.account.display_name_html,
}}
/>
<strong className='text-gray-800 dark:text-gray-200'>
<Emojify text={status.account.display_name} emojis={status.account.emojis} />
</strong>
</bdi>
</Link>
),
group: (
<Link to={`/groups/${group.id}`} className='hover:underline'>
<strong
className='text-gray-800 dark:text-gray-200'
dangerouslySetInnerHTML={{
__html: group.display_name_html,
}}
/>
<strong className='text-gray-800 dark:text-gray-200'>
<Emojify text={group.display_name} emojis={group.emojis} />
</strong>
</Link>
),
}}
@ -234,12 +229,9 @@ const Status: React.FC<IStatus> = (props) => {
const renderedAccounts = accounts.slice(0, 2).map(account => !!account && (
<Link key={account.acct} to={`/@${account.acct}`} className='hover:underline'>
<bdi className='truncate'>
<strong
className='text-gray-800 dark:text-gray-200'
dangerouslySetInnerHTML={{
__html: account.display_name_html,
}}
/>
<strong className='text-gray-800 dark:text-gray-200'>
<Emojify text={status.account.display_name} emojis={status.account.emojis} />
</strong>
</bdi>
</Link>
));
@ -294,7 +286,7 @@ const Status: React.FC<IStatus> = (props) => {
<Link to={`/groups/${group.id}`} className='hover:underline'>
<bdi className='truncate'>
<strong className='text-gray-800 dark:text-gray-200'>
<span dangerouslySetInnerHTML={{ __html: group.display_name_html }} />
<Emojify text={group.display_name} emojis={group.emojis} />
</strong>
</bdi>
</Link>

View file

@ -5,11 +5,9 @@ import { useRouteMatch } from 'react-router-dom';
import { groupComposeModal } from 'pl-fe/actions/compose';
import ThumbNavigationLink from 'pl-fe/components/thumb-navigation-link';
import Icon from 'pl-fe/components/ui/icon';
import { useStatContext } from 'pl-fe/contexts/stat-context';
import { Entities } from 'pl-fe/entity-store/entities';
import { useAppDispatch } from 'pl-fe/hooks/useAppDispatch';
import { useAppSelector } from 'pl-fe/hooks/useAppSelector';
import { useFeatures } from 'pl-fe/hooks/useFeatures';
import { useOwnAccount } from 'pl-fe/hooks/useOwnAccount';
import { useModalsStore } from 'pl-fe/stores/modals';
import { useUiStore } from 'pl-fe/stores/ui';
@ -28,13 +26,11 @@ const ThumbNavigation: React.FC = (): JSX.Element => {
const intl = useIntl();
const dispatch = useAppDispatch();
const { account } = useOwnAccount();
const features = useFeatures();
const match = useRouteMatch<{ groupId: string }>('/groups/:groupId');
const { openSidebar } = useUiStore();
const { openModal } = useModalsStore();
const { unreadChatsCount } = useStatContext();
const standalone = useAppSelector(isStandalone);
const notificationCount = useAppSelector((state) => state.notifications.unread);
@ -94,7 +90,7 @@ const ThumbNavigation: React.FC = (): JSX.Element => {
/>
)} */}
{account && !features.chats && composeButton}
{account && composeButton}
{(!standalone || account) && (
<ThumbNavigationLink
@ -115,21 +111,6 @@ const ThumbNavigation: React.FC = (): JSX.Element => {
count={notificationCount}
/>
)}
{account && features.chats && (
<>
<ThumbNavigationLink
src={require('@tabler/icons/outline/messages.svg')}
text={intl.formatMessage(messages.chats)}
to='/chats'
exact
count={unreadChatsCount}
countMax={9}
/>
{composeButton}
</>
)}
</div>
);
};

View file

@ -16,7 +16,7 @@ import { useSettings } from 'pl-fe/hooks/useSettings';
import type { Status } from 'pl-fe/normalizers/status';
interface ITranslateButton {
status: Pick<Status, 'id' | 'account' | 'contentHtml' | 'contentMapHtml' | 'language' | 'translating' | 'translation' | 'visibility'>;
status: Pick<Status, 'id' | 'account' | 'content' | 'content_map' | 'language' | 'translating' | 'translation' | 'visibility'>;
}
const TranslateButton: React.FC<ITranslateButton> = ({ status }) => {
@ -36,7 +36,7 @@ const TranslateButton: React.FC<ITranslateButton> = ({ status }) => {
allow_unauthenticated: allowUnauthenticated,
} = instance.pleroma.metadata.translation;
const renderTranslate = (me || allowUnauthenticated) && (allowRemote || status.account.local) && ['public', 'unlisted'].includes(status.visibility) && status.contentHtml.length > 0 && status.language !== null && intl.locale !== status.language && !status.contentMapHtml?.[intl.locale];
const renderTranslate = (me || allowUnauthenticated) && (allowRemote || status.account.local) && ['public', 'unlisted'].includes(status.visibility) && status.content.length > 0 && status.language !== null && intl.locale !== status.language && !status.content_map?.[intl.locale];
const supportsLanguages = (translationLanguages[status.language!]?.includes(intl.locale));
@ -64,7 +64,7 @@ const TranslateButton: React.FC<ITranslateButton> = ({ status }) => {
<Icon src={require('@tabler/icons/outline/language.svg')} className='size-4' />
<span>
{status.translation ? (
<FormattedMessage id='status.show_original' defaultMessage='Show original' />
<FormattedMessage id='status.hide_translation' defaultMessage='Hide translation' />
) : status.translating ? (
<FormattedMessage id='status.translating' defaultMessage='Translating…' />
) : (

View file

@ -5,6 +5,7 @@ import Account from 'pl-fe/components/account';
import Icon from 'pl-fe/components/icon';
import HStack from 'pl-fe/components/ui/hstack';
import Text from 'pl-fe/components/ui/text';
import Emojify from 'pl-fe/features/emoji/emojify';
import type { Account as AccountEntity } from 'pl-fe/normalizers/account';
@ -27,7 +28,7 @@ const MovedNote: React.FC<IMovedNote> = ({ from, to }) => (
id='notification.move'
defaultMessage='{name} moved to {targetName}'
values={{
name: <span dangerouslySetInnerHTML={{ __html: from.display_name_html }} />,
name: <span><Emojify text={from.display_name} emojis={from.emojis} /></span>,
targetName: to.acct,
}}
/>

View file

@ -2,16 +2,18 @@ import React from 'react';
import { FormattedDate, FormattedMessage, defineMessages, useIntl } from 'react-intl';
import { useAnnouncements } from 'pl-fe/api/hooks/admin/useAnnouncements';
import { ParsedContent } from 'pl-fe/components/parsed-content';
import ScrollableList from 'pl-fe/components/scrollable-list';
import Button from 'pl-fe/components/ui/button';
import Column from 'pl-fe/components/ui/column';
import HStack from 'pl-fe/components/ui/hstack';
import Stack from 'pl-fe/components/ui/stack';
import Text from 'pl-fe/components/ui/text';
import { AdminAnnouncement } from 'pl-fe/normalizers/announcement';
import { useModalsStore } from 'pl-fe/stores/modals';
import toast from 'pl-fe/toast';
import type { AdminAnnouncement } from 'pl-api';
const messages = defineMessages({
heading: { id: 'column.admin.announcements', defaultMessage: 'Announcements' },
deleteConfirm: { id: 'confirmations.admin.delete_announcement.confirm', defaultMessage: 'Delete' },
@ -47,7 +49,9 @@ const Announcement: React.FC<IAnnouncement> = ({ announcement }) => {
return (
<div key={announcement.id} className='rounded-lg bg-gray-100 p-4 dark:bg-primary-800'>
<Stack space={2}>
<Text dangerouslySetInnerHTML={{ __html: announcement.contentHtml }} />
<Text>
<ParsedContent html={announcement.content} emojis={announcement.emojis} />
</Text>
{(announcement.starts_at || announcement.ends_at || announcement.all_day) && (
<HStack space={2} wrap>
{announcement.starts_at && (

View file

@ -127,10 +127,9 @@ const Report: React.FC<IReport> = ({ id }) => {
<Stack>
{!!report.comment && report.comment.length > 0 && (
<Text
tag='blockquote'
dangerouslySetInnerHTML={{ __html: report.comment }}
/>
<Text tag='blockquote'>
{report.comment}
</Text>
)}
{!!account && (

View file

@ -4,6 +4,7 @@ import { useHistory } from 'react-router-dom';
import { useRelationship } from 'pl-fe/api/hooks/accounts/useRelationship';
import DropdownMenu from 'pl-fe/components/dropdown-menu';
import { ParsedContent } from 'pl-fe/components/parsed-content';
import RelativeTimestamp from 'pl-fe/components/relative-timestamp';
import Avatar from 'pl-fe/components/ui/avatar';
import HStack from 'pl-fe/components/ui/hstack';
@ -124,8 +125,9 @@ const ChatListItem: React.FC<IChatListItemInterface> = ({ chat, onClick }) => {
truncate
className='truncate-child pointer-events-none h-5 w-full'
data-testid='chat-last-message'
dangerouslySetInnerHTML={{ __html: chat.last_message?.content }}
/>
>
<ParsedContent html={chat.last_message?.content} emojis={chat.last_message.emojis} />
</Text>
)}
</>
)}

View file

@ -5,11 +5,11 @@ import React, { useMemo, useState } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import DropdownMenu from 'pl-fe/components/dropdown-menu';
import { ParsedContent } from 'pl-fe/components/parsed-content';
import HStack from 'pl-fe/components/ui/hstack';
import Icon from 'pl-fe/components/ui/icon';
import Stack from 'pl-fe/components/ui/stack';
import Text from 'pl-fe/components/ui/text';
import emojify from 'pl-fe/features/emoji';
import { MediaGallery } from 'pl-fe/features/ui/util/async-components';
import { useAppSelector } from 'pl-fe/hooks/useAppSelector';
import { ChatKeys, useChatActions } from 'pl-fe/queries/chats';
@ -18,7 +18,7 @@ import { useModalsStore } from 'pl-fe/stores/modals';
import { stripHTML } from 'pl-fe/utils/html';
import { onlyEmoji } from 'pl-fe/utils/rich-content';
import type { Chat, CustomEmoji } from 'pl-api';
import type { Chat } from 'pl-api';
import type { Menu as IMenu } from 'pl-fe/components/dropdown-menu';
import type { ChatMessage as ChatMessageEntity } from 'pl-fe/normalizers/chat-message';
@ -31,10 +31,6 @@ const messages = defineMessages({
const BIG_EMOJI_LIMIT = 3;
const makeEmojiMap = (record: ChatMessageEntity) =>
record.emojis.reduce((map: Record<string, CustomEmoji>, emoji: CustomEmoji) =>
(map[`:${emoji.shortcode}:`] = emoji, map), {});
const parsePendingContent = (content: string) => escape(content).replace(/(?:\r\n|\r|\n)/g, '<br>');
const parseContent = (chatMessage: ChatMessageEntity) => {
@ -42,8 +38,7 @@ const parseContent = (chatMessage: ChatMessageEntity) => {
const pending = chatMessage.pending;
const deleting = chatMessage.deleting;
const formatted = (pending && !deleting) ? parsePendingContent(content) : content;
const emojiMap = makeEmojiMap(chatMessage);
return emojify(formatted, emojiMap);
return formatted;
};
interface IChatMessage {
@ -244,12 +239,9 @@ const ChatMessage = (props: IChatMessage) => {
ref={setBubbleRef}
tabIndex={0}
>
<Text
size='sm'
theme='inherit'
className='break-word-nested'
dangerouslySetInnerHTML={{ __html: content }}
/>
<Text size='sm' theme='inherit' className='break-word-nested'>
<ParsedContent html={content} emojis={chatMessage.emojis} />
</Text>
</div>
</HStack>
)}

View file

@ -67,11 +67,14 @@ interface IComposeForm<ID extends string> {
clickableAreaRef?: React.RefObject<HTMLDivElement>;
event?: string;
group?: string;
extra?: React.ReactNode;
onSubmit?: () => void;
fullScreen?: boolean;
withAvatar?: boolean;
transparent?: boolean;
}
const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickableAreaRef, event, group, withAvatar, transparent }: IComposeForm<ID>) => {
const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickableAreaRef, event, group, extra, onSubmit, fullScreen, withAvatar, transparent }: IComposeForm<ID>) => {
const history = useHistory();
const intl = useIntl();
const dispatch = useAppDispatch();
@ -106,8 +109,17 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
const { isDraggedOver } = useDraggedFiles(formRef);
// const text = editorRef.current?.getEditorState().read($createRemarkExport({
// handlers: {
// hashtag: (node) => ({ type: 'text', value: node.getTextContent() }),
// mention: (node) => ({ type: 'text', value: node.getTextContent() }),
// },
// })) ?? '';
const fulltext = [spoilerText, countableText(text)].join('');
const characterCountProgress = length(text) / maxTootChars;
const isEmpty = !(fulltext.trim() || anyMedia);
const condensed = shouldCondense && !isDraggedOver && !composeFocused && isEmpty && !isUploading;
const shouldAutoFocus = autoFocus && !showSearch;
@ -142,7 +154,8 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
if (!canSubmit) return;
e?.preventDefault();
dispatch(submitCompose(id, { history })).then(() => {
dispatch(submitCompose(id, { history, onSubmit, propagate: fullScreen })).then(() => {
editorRef.current?.dispatchCommand(CLEAR_EDITOR_COMMAND, undefined);
}).catch(() => {});
};
@ -226,7 +239,7 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
if (features.postLanguages) selectButtons.push(<LanguageDropdown key='language-dropdown' composeId={id} />);
return (
<Stack className='w-full' space={4} ref={formRef} onClick={handleClick} element='form' onSubmit={handleSubmit}>
<Stack className='w-full' grow={fullScreen} space={4} ref={formRef} onClick={handleClick} element='form' onSubmit={handleSubmit}>
<WarningContainer composeId={id} />
{!shouldCondense && !event && !group && groupId && <ReplyGroupIndicator composeId={id} />}
@ -268,6 +281,7 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
handleSubmit={handleSubmit}
onFocus={handleComposeFocus}
onPaste={onPaste}
fullScreen
/>
</Suspense>
</div>
@ -285,7 +299,7 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
{renderButtons()}
<HStack space={4} alignItems='center' className='ml-auto rtl:ml-0 rtl:mr-auto'>
{maxTootChars && (
{characterCountProgress >= 0.2 && maxTootChars && (
<HStack space={1} alignItems='center'>
<TextCharacterCounter max={maxTootChars} text={text} />
<VisualCharacterCounter max={maxTootChars} text={text} />

View file

@ -3,6 +3,7 @@ import { FormattedMessage } from 'react-intl';
import Link from 'pl-fe/components/link';
import Text from 'pl-fe/components/ui/text';
import Emojify from 'pl-fe/features/emoji/emojify';
import { useAppSelector } from 'pl-fe/hooks/useAppSelector';
import { makeGetStatus } from 'pl-fe/selectors';
@ -28,10 +29,11 @@ const ReplyGroupIndicator = (props: IReplyGroupIndicator) => {
id='compose.reply_group_indicator.message'
defaultMessage='Posting to {groupLink}'
values={{
groupLink: <Link
to={`/groups/${group.id}`}
dangerouslySetInnerHTML={{ __html: group.display_name_html }}
/>,
groupLink: (
<Link to={`/groups/${group.id}`}>
<Emojify text={group.display_name} emojis={group.emojis} />
</Link>
),
}}
/>
</Text>

View file

@ -12,7 +12,7 @@ import type { Status } from 'pl-fe/normalizers/status';
interface IReplyIndicator {
className?: string;
status?: Pick<Status, 'account_id' | 'contentHtml' | 'created_at' | 'hidden' | 'media_attachments' | 'mentions' | 'search_index' | 'sensitive' | 'spoiler_text' | 'quote_id'>;
status?: Pick<Status, 'account_id' | 'content' | 'created_at' | 'emojis' | 'hidden' | 'media_attachments' | 'mentions' | 'search_index' | 'sensitive' | 'spoiler_text' | 'quote_id'>;
onCancel?: () => void;
hideActions: boolean;
}
@ -52,7 +52,7 @@ const ReplyIndicator: React.FC<IReplyIndicator> = ({ className, status, hideActi
size='sm'
direction={getTextDirection(status.search_index)}
>
<ParsedContent html={status.contentHtml} mentions={status.mentions} hasQuote={!!status.quote_id} />
<ParsedContent html={status.content} mentions={status.mentions} hasQuote={!!status.quote_id} emojis={status.emojis} />
</Markup>
{status.media_attachments.length > 0 && (

View file

@ -62,6 +62,7 @@ interface IComposeEditor {
onChange?(text: string): void;
onFocus?: React.FocusEventHandler<HTMLDivElement>;
placeholder?: string;
fullScreen?: boolean;
}
const theme: InitialConfigType['theme'] = {
@ -97,6 +98,7 @@ const ComposeEditor = React.forwardRef<LexicalEditor, IComposeEditor>(({
onFocus,
onPaste,
placeholder,
fullScreen,
}, ref) => {
const dispatch = useAppDispatch();
const { content_type: contentType } = useCompose(composeId);
@ -196,16 +198,17 @@ const ComposeEditor = React.forwardRef<LexicalEditor, IComposeEditor>(({
return (
<LexicalComposer key={isWysiwyg ? 'wysiwyg' : ''} initialConfig={initialConfig}>
<div className={clsx('lexical relative', className)} data-markup>
<div className={clsx('lexical relative', fullScreen && 'h-full', className)} data-markup>
<RichTextPlugin
contentEditable={
<div onFocus={onFocus} onPaste={handlePaste} ref={onRef}>
<div className={clsx(fullScreen && 'h-full')} onFocus={onFocus} onPaste={handlePaste} ref={onRef}>
<ContentEditable
tabIndex={0}
className={clsx(
'relative z-10 text-[1rem] outline-none transition-[min-height] motion-reduce:transition-none',
editableClassName,
{
'h-full': fullScreen,
'min-h-[39px]': condensed,
'min-h-[99px]': !condensed,
},
@ -216,7 +219,7 @@ const ComposeEditor = React.forwardRef<LexicalEditor, IComposeEditor>(({
placeholder={(
<div
className={clsx(
'pointer-events-none absolute top-0 select-none text-[1rem] text-gray-600 dark:placeholder:text-gray-600',
'pointer-events-none absolute select-none text-[1rem] text-gray-600 dark:placeholder:text-gray-600',
placeholderClassName,
)}
>

View file

@ -0,0 +1,108 @@
/* eslint-disable promise/catch-or-return */
import React, { useEffect, useMemo } from 'react';
import { FormattedMessage, useIntl } from 'react-intl';
import { useLocation } from 'react-router-dom';
import { COMPOSE_QUOTE, COMPOSE_REPLY, setComposeToStatus } from 'pl-fe/actions/compose';
import { fetchDraftStatuses } from 'pl-fe/actions/draft-statuses';
import { fetchStatus } from 'pl-fe/actions/statuses';
import Stack from 'pl-fe/components/ui/stack';
import ComposeForm from 'pl-fe/features/compose/components/compose-form';
import { useAppDispatch } from 'pl-fe/hooks/useAppDispatch';
import { useCompose } from 'pl-fe/hooks/useCompose';
import { useFeatures } from 'pl-fe/hooks/useFeatures';
import { makeGetStatus, selectOwnAccount } from 'pl-fe/selectors';
import { useSettingsStore } from 'pl-fe/stores/settings';
import { buildStatus as buildDraftStatus } from '../draft-statuses/builder';
const getStatus = makeGetStatus();
const ComposePage = () => {
const { search } = useLocation();
const params = new URLSearchParams(search);
const dispatch = useAppDispatch();
const intl = useIntl();
const { createStatusExplicitAddressing } = useFeatures();
const { settings } = useSettingsStore();
const compose = useCompose('compose-modal');
const { id: statusId, privacy, group_id: groupId } = compose!;
const inReplyTo = params.get('in_reply_to');
const quote = params.get('quote');
const draftId = params.get('draft_id');
const heading = useMemo(() => {
if (statusId) {
return <FormattedMessage id='navigation_bar.compose_edit' defaultMessage='Edit post' />;
} else if (privacy === 'direct') {
return <FormattedMessage id='navigation_bar.compose_direct' defaultMessage='Direct message' />;
} else if (inReplyTo && groupId) {
return <FormattedMessage id='navigation_bar.compose_group_reply' defaultMessage='Reply to group post' />;
} else if (groupId) {
return <FormattedMessage id='navigation_bar.compose_group' defaultMessage='Compose to group' />;
} else if (inReplyTo) {
return <FormattedMessage id='navigation_bar.compose_reply' defaultMessage='Reply to post' />;
} else if (quote) {
return <FormattedMessage id='navigation_bar.compose_quote' defaultMessage='Quote post' />;
} else {
return <FormattedMessage id='navigation_bar.compose' defaultMessage='Compose new post' />;
}
}, []);
useEffect(() => {
if (inReplyTo) dispatch(fetchStatus(inReplyTo, intl))
.then((_) => dispatch((dispatch, getState) => {
const state = getState();
const status = getStatus(state, { id: inReplyTo });
dispatch({
type: COMPOSE_REPLY,
id: 'compose-modal',
status: status,
account: selectOwnAccount(state),
explicitAddressing: createStatusExplicitAddressing,
preserveSpoilers: settings.preserveSpoilers,
});
}));
else if (quote) dispatch(fetchStatus(quote, intl))
.then((_) => dispatch((dispatch, getState) => {
const state = getState();
const status = getStatus(state, { id: quote });
dispatch({
type: COMPOSE_QUOTE,
id: 'compose-modal',
status: status,
account: selectOwnAccount(state),
explicitAddressing: createStatusExplicitAddressing,
});
}));
else if (draftId) dispatch(fetchDraftStatuses())
.then(() => dispatch((dispatch, getState) => {
const state = getState();
const draftStatus = state.draft_statuses.get(draftId);
if (draftStatus) {
const status = buildDraftStatus(state, draftStatus);
dispatch(setComposeToStatus(status as any, status.poll, draftStatus.text, draftStatus.spoiler_text, draftStatus.content_type, false, draftStatus.draft_id, draftStatus.editorState));
}
}));
}, []);
return (
<Stack className='h-full bg-white p-4 black:bg-black dark:bg-gray-900' space={2}>
<h3 className='grow-0 truncate text-lg font-bold leading-6 text-gray-900 dark:text-white'>
{heading}
</h3>
<ComposeForm id='compose-modal' onSubmit={() => window.close()} fullScreen />
</Stack>
);
};
export default ComposePage;

View file

@ -70,13 +70,13 @@ const AccountCard: React.FC<IAccountCard> = ({ id }) => {
withRelationship={false}
/>
{!!account.note_emojified && (
{!!account.note && (
<Text
truncate
align='left'
className='line-clamp-2 inline text-ellipsis [&_br]:hidden [&_p:first-child]:inline [&_p:first-child]:truncate [&_p]:hidden'
>
<ParsedContent html={account.note_emojified} />
<ParsedContent html={account.note} emojis={account.emojis} />
</Text>
)}
</Stack>

View file

@ -6,6 +6,7 @@ import { cancelDraftStatus } from 'pl-fe/actions/draft-statuses';
import Button from 'pl-fe/components/ui/button';
import HStack from 'pl-fe/components/ui/hstack';
import { useAppDispatch } from 'pl-fe/hooks/useAppDispatch';
import { userTouching } from 'pl-fe/is-mobile';
import { useModalsStore } from 'pl-fe/stores/modals';
import { useSettingsStore } from 'pl-fe/stores/settings';
@ -31,23 +32,21 @@ const DraftStatusActionBar: React.FC<IDraftStatusActionBar> = ({ source, status
const dispatch = useAppDispatch();
const handleCancelClick = () => {
dispatch((_, getState) => {
const deleteModal = settings.deleteModal;
if (!deleteModal) {
dispatch(cancelDraftStatus(source.draft_id));
} else {
openModal('CONFIRM', {
heading: intl.formatMessage(messages.deleteHeading),
message: intl.formatMessage(messages.deleteMessage),
confirm: intl.formatMessage(messages.deleteConfirm),
onConfirm: () => dispatch(cancelDraftStatus(source.draft_id)),
});
}
});
const deleteModal = settings.deleteModal;
if (!deleteModal) {
dispatch(cancelDraftStatus(source.draft_id));
} else {
openModal('CONFIRM', {
heading: intl.formatMessage(messages.deleteHeading),
message: intl.formatMessage(messages.deleteMessage),
confirm: intl.formatMessage(messages.deleteConfirm),
onConfirm: () => dispatch(cancelDraftStatus(source.draft_id)),
});
}
};
const handleEditClick = () => {
if (!userTouching.matches) return window.open(`/compose?draft_id=${source.draft_id}`, 'targetWindow', 'height=500,width=700');
dispatch(setComposeToStatus(status, status.poll, source.text, source.spoiler_text, source.content_type, false, source.draft_id, source.editorState));
openModal('COMPOSE');
};

View file

@ -0,0 +1,103 @@
import split from 'graphemesplit';
import React from 'react';
import { makeEmojiMap } from 'pl-fe/utils/normalizers';
import unicodeMapping from './mapping';
import { validEmojiChar } from '.';
import type { CustomEmoji } from 'pl-api';
interface IMaybeEmoji {
text: string;
emojis: Record<string, CustomEmoji>;
}
const MaybeEmoji: React.FC<IMaybeEmoji> = ({ text, emojis }) => {
if (text.length < 3) return text;
if (text in emojis) {
const emoji = emojis[text];
const filename = emoji.static_url;
if (filename?.length > 0) {
return <img draggable={false} className='emojione' alt={text} title={text} src={filename} />;
}
}
return text;
};
interface IEmojify {
text: string;
emojis?: Array<CustomEmoji> | Record<string, CustomEmoji>;
}
const Emojify: React.FC<IEmojify> = ({ text, emojis = {} }) => React.useMemo(() => {
if (Array.isArray(emojis)) emojis = makeEmojiMap(emojis);
const nodes = [];
let stack = '';
let open = false;
const clearStack = () => {
if (stack.length) nodes.push(stack);
open = false;
stack = '';
};
for (let c of split(text)) {
// convert FE0E selector to FE0F so it can be found in unimap
if (c.codePointAt(c.length - 1) === 65038) {
c = c.slice(0, -1) + String.fromCodePoint(65039);
}
// unqualified emojis aren't in emoji-mart's mappings so we just add FEOF
const unqualified = c + String.fromCodePoint(65039);
if (c in unicodeMapping) {
clearStack();
const { unified, shortcode } = unicodeMapping[c];
nodes.push(
<img draggable={false} className='emojione' alt={c} title={`:${shortcode}:`} src={`/packs/emoji/${unified}.svg`} />,
);
} else if (unqualified in unicodeMapping) {
clearStack();
const { unified, shortcode } = unicodeMapping[unqualified];
nodes.push(
<img draggable={false} className='emojione' alt={unqualified} title={`:${shortcode}:`} src={`/packs/emoji/${unified}.svg`} />,
);
} else if (c === ':') {
if (!open) {
clearStack();
}
stack += ':';
// we see another : we convert it and clear the stack buffer
if (open) {
nodes.push(<MaybeEmoji text={stack} emojis={emojis} />);
stack = '';
}
open = !open;
} else {
stack += c;
if (open && !validEmojiChar(c)) {
clearStack();
}
}
}
if (stack.length) nodes.push(stack);
return nodes;
}, [text, emojis]);
export { Emojify as default };

View file

@ -1,7 +1,3 @@
import split from 'graphemesplit';
import unicodeMapping from './mapping';
import type { Emoji as EmojiMart, CustomEmoji as EmojiMartCustom } from './data';
import type { CustomEmoji as BaseCustomEmoji } from 'pl-api';
@ -57,148 +53,6 @@ const isAlphaNumeric = (c: string) => {
const validEmojiChar = (c: string) =>
isAlphaNumeric(c) || ['_', '-', '.'].includes(c);
const convertCustom = (shortname: string, filename: string) =>
`<img draggable="false" class="emojione transition-transform ease-linear no-reduce-motion:hover:scale-125" alt="${shortname}" title="${shortname}" src="${filename}" />`;
const convertUnicode = (c: string) => {
const { unified, shortcode } = unicodeMapping[c];
return `<img draggable="false" class="emojione transition-transform ease-linear no-reduce-motion:hover:scale-125" alt="${c}" title=":${shortcode}:" src="/packs/emoji/${unified}.svg" />`;
};
const convertEmoji = (str: string, customEmojis: any) => {
if (str.length < 3) return str;
if (str in customEmojis) {
const emoji = customEmojis[str];
const filename = emoji.static_url;
if (filename?.length > 0) {
return convertCustom(str, filename);
}
}
return str;
};
const emojifyText = (str: string, customEmojis = {}) => {
let buf = '';
let stack = '';
let open = false;
const clearStack = () => {
buf += stack;
open = false;
stack = '';
};
for (let c of split(str)) {
// convert FE0E selector to FE0F so it can be found in unimap
if (c.codePointAt(c.length - 1) === 65038) {
c = c.slice(0, -1) + String.fromCodePoint(65039);
}
// unqualified emojis aren't in emoji-mart's mappings so we just add FEOF
const unqualified = c + String.fromCodePoint(65039);
if (c in unicodeMapping) {
if (open) { // unicode emoji inside colon
clearStack();
}
buf += convertUnicode(c);
} else if (unqualified in unicodeMapping) {
if (open) { // unicode emoji inside colon
clearStack();
}
buf += convertUnicode(unqualified);
} else if (c === ':') {
stack += ':';
// we see another : we convert it and clear the stack buffer
if (open) {
buf += convertEmoji(stack, customEmojis);
stack = '';
}
open = !open;
} else {
if (open) {
stack += c;
// if the stack is non-null and we see invalid chars it's a string not emoji
// so we push it to the return result and clear it
if (!validEmojiChar(c)) {
clearStack();
}
} else {
buf += c;
}
}
}
// never found a closing colon so it's just a raw string
if (open) {
buf += stack;
}
return buf;
};
const parseHTML = (str: string): { text: boolean; data: string }[] => {
const tokens = [];
let buf = '';
let stack = '';
let open = false;
for (const c of str) {
if (c === '<') {
if (open) {
tokens.push({ text: true, data: stack });
stack = '<';
} else {
tokens.push({ text: true, data: buf });
stack = '<';
open = true;
}
} else if (c === '>') {
if (open) {
open = false;
tokens.push({ text: false, data: stack + '>' });
stack = '';
buf = '';
} else {
buf += '>';
}
} else {
if (open) {
stack += c;
} else {
buf += c;
}
}
}
if (open) {
tokens.push({ text: true, data: buf + stack });
} else if (buf !== '') {
tokens.push({ text: true, data: buf });
}
return tokens;
};
const emojify = (str: string, customEmojis: Record<string, BaseCustomEmoji> = {}) =>
parseHTML(str)
.map(({ text, data }) => {
if (!text) return data;
if (data.length === 0 || data === ' ') return data;
return emojifyText(data, customEmojis);
})
.join('');
const buildCustomEmojis = (customEmojis: Array<BaseCustomEmoji>) => {
const emojis: EmojiMart<EmojiMartCustom>[] = [];
@ -225,5 +79,5 @@ export {
isCustomEmoji,
isNativeEmoji,
buildCustomEmojis,
emojify as default,
validEmojiChar,
};

View file

@ -19,6 +19,7 @@ import IconButton from 'pl-fe/components/ui/icon-button';
import Stack from 'pl-fe/components/ui/stack';
import Text from 'pl-fe/components/ui/text';
import VerificationBadge from 'pl-fe/components/verification-badge';
import Emojify from 'pl-fe/features/emoji/emojify';
import { useAppDispatch } from 'pl-fe/hooks/useAppDispatch';
import { useFeatures } from 'pl-fe/hooks/useFeatures';
import { useOwnAccount } from 'pl-fe/hooks/useOwnAccount';
@ -414,7 +415,7 @@ const EventHeader: React.FC<IEventHeader> = ({ status }) => {
name: (
<Link className='mention inline-block' to={`/@${account.acct}`}>
<HStack space={1} alignItems='center' grow>
<span dangerouslySetInnerHTML={{ __html: account.display_name_html }} />
<span><Emojify text={account.display_name} emojis={account.emojis} /></span>
{account.verified && <VerificationBadge />}
</HStack>
</Link>

View file

@ -181,7 +181,7 @@ const EventInformation: React.FC<IEventInformation> = ({ params }) => {
return (
<Stack className='mt-4 sm:p-2' space={2}>
{!!status.contentHtml.trim() && (
{!!status.content.trim() && (
<Stack space={1}>
<Text size='xl' weight='bold'>
<FormattedMessage id='event.description' defaultMessage='Description' />

View file

@ -10,6 +10,7 @@ import Text from 'pl-fe/components/ui/text';
import VerificationBadge from 'pl-fe/components/verification-badge';
import { useAppSelector } from 'pl-fe/hooks/useAppSelector';
import Emojify from '../emoji/emojify';
import ActionButton from '../ui/components/action-button';
import { HotKeys } from '../ui/components/hotkeys';
@ -41,14 +42,9 @@ const SuggestionItem: React.FC<ISuggestionItem> = ({ accountId }) => {
<Stack>
<HStack alignItems='center' justifyContent='center' space={1}>
<Text
weight='semibold'
dangerouslySetInnerHTML={{ __html: account.display_name_html }}
truncate
align='center'
size='sm'
className='max-w-[95%]'
/>
<Text weight='semibold' truncate align='center' size='sm' className='max-w-[95%]'>
<Emojify text={account.display_name} emojis={account.emojis} />
</Text>
{account.verified && <VerificationBadge />}
</HStack>

View file

@ -10,6 +10,7 @@ import HStack from 'pl-fe/components/ui/hstack';
import Icon from 'pl-fe/components/ui/icon';
import Stack from 'pl-fe/components/ui/stack';
import Text from 'pl-fe/components/ui/text';
import Emojify from 'pl-fe/features/emoji/emojify';
import { useModalsStore } from 'pl-fe/stores/modals';
import { isDefaultHeader } from 'pl-fe/utils/accounts';
@ -141,9 +142,10 @@ const GroupHeader: React.FC<IGroupHeader> = ({ group }) => {
<Text
size='xl'
weight='bold'
dangerouslySetInnerHTML={{ __html: group.display_name_html }}
data-testid='group-name'
/>
>
<Emojify text={group.display_name} emojis={group.emojis} />
</Text>
<Stack data-testid='group-meta' space={1} alignItems='center'>
<HStack className='text-gray-700 dark:text-gray-600' space={2} wrap>
@ -157,7 +159,7 @@ const GroupHeader: React.FC<IGroupHeader> = ({ group }) => {
align='center'
className='[&_a]:text-primary-600 [&_a]:hover:underline [&_a]:dark:text-accent-blue'
>
<ParsedContent html={group.note_emojified} />
<ParsedContent html={group.note} emojis={group.emojis} />
</Text>
</Stack>

View file

@ -18,6 +18,7 @@ import { useAppSelector } from 'pl-fe/hooks/useAppSelector';
import { useInstance } from 'pl-fe/hooks/useInstance';
import toast from 'pl-fe/toast';
import { isDefaultAvatar, isDefaultHeader } from 'pl-fe/utils/accounts';
import { unescapeHTML } from 'pl-fe/utils/html';
import AvatarPicker from '../edit-profile/components/avatar-picker';
import HeaderPicker from '../edit-profile/components/header-picker';
@ -51,7 +52,7 @@ const EditGroup: React.FC<IEditGroup> = ({ params: { groupId } }) => {
const header = useImageField({ maxPixels: 1920 * 1080, preview: nonDefaultHeader(group?.header) });
const displayName = useTextField(group?.display_name);
const note = useTextField(group?.note_plain);
const note = useTextField(unescapeHTML(group?.note));
const maxName = Number(instance.configuration.groups.max_characters_name);
const maxNote = Number(instance.configuration.groups.max_characters_description);

View file

@ -13,6 +13,7 @@ import Text from 'pl-fe/components/ui/text';
import { useModalsStore } from 'pl-fe/stores/modals';
import toast from 'pl-fe/toast';
import Emojify from '../emoji/emojify';
import ColumnForbidden from '../ui/components/column-forbidden';
type RouteParams = { groupId: string };
@ -86,7 +87,7 @@ const ManageGroup: React.FC<IManageGroup> = ({ params }) => {
<List>
<ListItem label={intl.formatMessage(messages.editGroup)} to={`/groups/${group.id}/manage/edit`}>
<span dangerouslySetInnerHTML={{ __html: group.display_name_html }} />
<span><Emojify text={group.display_name} emojis={group.emojis} /></span>
</ListItem>
</List>
</>

View file

@ -7,13 +7,14 @@ import HStack from 'pl-fe/components/ui/hstack';
import Icon from 'pl-fe/components/ui/icon';
import Stack from 'pl-fe/components/ui/stack';
import Text from 'pl-fe/components/ui/text';
import Emojify from 'pl-fe/features/emoji/emojify';
import GroupActionButton from 'pl-fe/features/group/components/group-action-button';
import { shortNumberFormat } from 'pl-fe/utils/numbers';
import type { Group } from 'pl-fe/normalizers/group';
interface IGroupListItem {
group: Pick<Group, 'id' | 'avatar' | 'avatar_description' | 'display_name_html' | 'locked' | 'members_count' | 'relationship'>;
group: Pick<Group, 'id' | 'avatar' | 'avatar_description' | 'display_name' | 'emojis' | 'locked' | 'members_count' | 'relationship'>;
withJoinAction?: boolean;
}
@ -34,11 +35,9 @@ const GroupListItem = (props: IGroupListItem) => {
/>
<Stack className='overflow-hidden'>
<Text
weight='bold'
dangerouslySetInnerHTML={{ __html: group.display_name_html }}
truncate
/>
<Text weight='bold' truncate>
<Emojify text={group.display_name} emojis={group.emojis} />
</Text>
<HStack className='text-gray-700 dark:text-gray-600' space={1} alignItems='center'>
<Icon

View file

@ -5,9 +5,11 @@ import {
importFollows,
importBlocks,
importMutes,
importArchive,
} from 'pl-fe/actions/import-data';
import Column from 'pl-fe/components/ui/column';
import { useFeatures } from 'pl-fe/hooks/useFeatures';
import { useInstance } from 'pl-fe/hooks/useInstance';
import DataImporter from './components/data-importer';
@ -34,8 +36,15 @@ const muteMessages = defineMessages({
submit: { id: 'import_data.actions.import_mutes', defaultMessage: 'Import mutes' },
});
const archiveMessages = defineMessages({
input_label: { id: 'import_data.archive_label', defaultMessage: 'Archive' },
input_hint: { id: 'import_data.hints.archive', defaultMessage: 'Archive containing an archive of statuses' },
submit: { id: 'import_data.actions.import_archive', defaultMessage: 'Import archive' },
});
const ImportData = () => {
const intl = useIntl();
const instance = useInstance();
const features = useFeatures();
return (
@ -43,6 +52,9 @@ const ImportData = () => {
{features.importFollows && <DataImporter action={importFollows} messages={followMessages} allowOverwrite={features.importOverwrite} />}
{features.importBlocks && <DataImporter action={importBlocks} messages={blockMessages} allowOverwrite={features.importOverwrite} />}
{features.importMutes && <DataImporter action={importMutes} messages={muteMessages} />}
{instance.pleroma.metadata.features.includes('bigbuffet') && (
<DataImporter action={importArchive} messages={archiveMessages} accept='.tar,.tar.gz,.zip' />
)}
</Column>
);
};

View file

@ -1,7 +1,7 @@
import DOMPurify from 'isomorphic-dompurify';
import React, { useMemo } from 'react';
import React from 'react';
import Markup from 'pl-fe/components/markup';
import { ParsedContent } from 'pl-fe/components/parsed-content';
import Stack from 'pl-fe/components/ui/stack';
import { useInstance } from 'pl-fe/hooks/useInstance';
import { getTextDirection } from 'pl-fe/utils/rtl';
@ -10,7 +10,6 @@ import { LogoText } from './logo-text';
const SiteBanner: React.FC = () => {
const instance = useInstance();
const description = useMemo(() => DOMPurify.sanitize(instance.description), [instance.description]);
return (
<Stack space={6}>
@ -21,9 +20,10 @@ const SiteBanner: React.FC = () => {
{instance.description.trim().length > 0 && (
<Markup
size='lg'
dangerouslySetInnerHTML={{ __html: description }}
direction={getTextDirection(description)}
/>
direction={getTextDirection(instance.description)}
>
<ParsedContent html={instance.description} />
</Markup>
)}
</Stack>
);

View file

@ -50,7 +50,7 @@ const LandingTimeline = () => {
{timelineEnabled ? (
<PullToRefresh onRefresh={handleRefresh}>
<Timeline
listClassName='black:p-0 black:sm:p-4 black:sm:pt-0'
className='black:p-0 black:sm:p-4 black:sm:pt-0'
loadMoreClassName='black:sm:mx-4'
scrollKey={`${timelineId}_timeline`}
timelineId={timelineId}

View file

@ -13,6 +13,7 @@ import HStack from 'pl-fe/components/ui/hstack';
import Text from 'pl-fe/components/ui/text';
import AccountContainer from 'pl-fe/containers/account-container';
import StatusContainer from 'pl-fe/containers/status-container';
import Emojify from 'pl-fe/features/emoji/emojify';
import { HotKeys } from 'pl-fe/features/ui/components/hotkeys';
import { useAppDispatch } from 'pl-fe/hooks/useAppDispatch';
import { useAppSelector } from 'pl-fe/hooks/useAppSelector';
@ -37,14 +38,15 @@ const notificationForScreenReader = (intl: IntlShape, message: string, timestamp
return output.join(', ');
};
const buildLink = (account: Pick<Account, 'acct' | 'display_name_html' | 'id'>): JSX.Element => (
const buildLink = (account: Pick<Account, 'acct' | 'display_name' | 'emojis' | 'id'>): JSX.Element => (
<HoverAccountWrapper key={account.acct} element='bdi' accountId={account.id}>
<Link
className='font-bold text-gray-800 hover:underline dark:text-gray-200'
title={account.acct}
to={`/@${account.acct}`}
dangerouslySetInnerHTML={{ __html: account.display_name_html }}
/>
>
<Emojify text={account.display_name} emojis={account.emojis} />
</Link>
</HoverAccountWrapper>
);
@ -153,7 +155,7 @@ const messages: Record<NotificationType | 'reply', MessageDescriptor> = defineMe
const buildMessage = (
intl: IntlShape,
type: NotificationType | 'reply',
accounts: Array<Pick<Account, 'acct' | 'display_name_html' | 'id'>>,
accounts: Array<Pick<Account, 'acct' | 'display_name' | 'emojis' | 'id'>>,
targetName: string,
instanceTitle: string,
): React.ReactNode => {

View file

@ -214,6 +214,12 @@ const Preferences = () => {
>
<SettingToggle settings={settings} settingPath={['demetricator']} onChange={onToggleChange} />
</ListItem>
{features.emojiReacts && (
<ListItem label={<FormattedMessage id='preferences.fields.wrench_label' defaultMessage='Display wrench reaction button' />} >
<SettingToggle settings={settings} settingPath={['showWrenchButton']} onChange={onToggleChange} />
</ListItem>
)}
</List>
<List>

View file

@ -1,5 +1,5 @@
import React, { useEffect } from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { defineMessages, useIntl } from 'react-intl';
import { fetchMfa } from 'pl-fe/actions/mfa';
import List, { ListItem } from 'pl-fe/components/list';
@ -13,7 +13,7 @@ import { useOwnAccount } from 'pl-fe/hooks/useOwnAccount';
import Preferences from '../preferences';
import MessagesSettings from './components/messages-settings';
// import MessagesSettings from './components/messages-settings';
const any = (arr: Array<any>): boolean => arr.some(Boolean);
@ -126,7 +126,7 @@ const Settings = () => {
</>
)}
{features.chats ? (
{/* {features.chats ? (
<>
<CardHeader>
<CardTitle title={<FormattedMessage id='column.chats' defaultMessage='Chats' />} />
@ -136,7 +136,7 @@ const Settings = () => {
<MessagesSettings />
</CardBody>
</>
) : null}
) : null} */}
<CardHeader>
<CardTitle title={intl.formatMessage(messages.preferences)} />

View file

@ -1,5 +1,5 @@
import React, { useRef } from 'react';
import { FormattedDate, FormattedMessage, useIntl } from 'react-intl';
import { defineMessages, FormattedDate, FormattedMessage, useIntl } from 'react-intl';
import { Link } from 'react-router-dom';
import Account from 'pl-fe/components/account';
@ -15,6 +15,7 @@ import HStack from 'pl-fe/components/ui/hstack';
import Icon from 'pl-fe/components/ui/icon';
import Stack from 'pl-fe/components/ui/stack';
import Text from 'pl-fe/components/ui/text';
import Emojify from 'pl-fe/features/emoji/emojify';
import QuotedStatus from 'pl-fe/features/status/containers/quoted-status-container';
import StatusInteractionBar from './status-interaction-bar';
@ -22,6 +23,10 @@ import StatusTypeIcon from './status-type-icon';
import type { SelectedStatus } from 'pl-fe/selectors';
const messages = defineMessages({
applicationName: { id: 'status.application_name', defaultMessage: 'Sent from {name}' },
});
interface IDetailedStatus {
status: SelectedStatus;
withMedia?: boolean;
@ -62,7 +67,7 @@ const DetailedStatus: React.FC<IDetailedStatus> = ({
<Link to={`/groups/${status.group.id}`} className='hover:underline'>
<bdi className='truncate'>
<strong className='text-gray-800 dark:text-gray-200'>
<span dangerouslySetInnerHTML={{ __html: status.group.display_name_html }} />
<Emojify text={status.account.display_name} emojis={status.account.emojis} />
</strong>
</bdi>
</Link>
@ -144,35 +149,40 @@ const DetailedStatus: React.FC<IDetailedStatus> = ({
<HStack space={1} alignItems='center'>
<span>
<a href={actualStatus.url} target='_blank' rel='noopener' className='hover:underline'>
<Text tag='span' theme='muted' size='sm'>
<Text tag='span' theme='muted' size='sm'>
<a href={actualStatus.url} target='_blank' rel='noopener' className='hover:underline'>
<FormattedDate value={new Date(actualStatus.created_at)} hour12 year='numeric' month='short' day='2-digit' hour='numeric' minute='2-digit' />
</Text>
</a>
{actualStatus.application && (
<a href={(actualStatus.application.website) ? actualStatus.application.website : '#' } target='_blank' rel='noopener' className='hover:underline ml-2'>
<Text tag='span' theme='muted' size='sm'>
({actualStatus.application.name})
</Text>
</a>
)}
{actualStatus.edited_at && (
<>
{' · '}
<div
className='inline hover:underline'
onClick={handleOpenCompareHistoryModal}
role='button'
tabIndex={0}
>
<Text tag='span' theme='muted' size='sm'>
{actualStatus.application && (
<>
{' · '}
<a
href={(actualStatus.application.website) ? actualStatus.application.website : '#'}
target='_blank'
rel='noopener'
className='hover:underline'
title={intl.formatMessage(messages.applicationName, { name: actualStatus.application.name })}
>
{actualStatus.application.name}
</a>
</>
)}
{actualStatus.edited_at && (
<>
{' · '}
<div
className='inline hover:underline'
onClick={handleOpenCompareHistoryModal}
role='button'
tabIndex={0}
>
<FormattedMessage id='status.edited' defaultMessage='Edited {date}' values={{ date: intl.formatDate(new Date(actualStatus.edited_at), { hour12: true, month: 'short', day: '2-digit', hour: 'numeric', minute: '2-digit' }) }} />
</Text>
</div>
</>
)}
</div>
</>
)}
</Text>
</span>
<StatusTypeIcon status={actualStatus} />

View file

@ -8,6 +8,7 @@ import Avatar from 'pl-fe/components/ui/avatar';
import Button from 'pl-fe/components/ui/button';
import HStack from 'pl-fe/components/ui/hstack';
import { useAppDispatch } from 'pl-fe/hooks/useAppDispatch';
import { userTouching } from 'pl-fe/is-mobile';
import { useModalsStore } from 'pl-fe/stores/modals';
const ComposeButton = () => {
@ -26,7 +27,10 @@ const ComposeButton = () => {
const HomeComposeButton = () => {
const { openModal } = useModalsStore();
const onOpenCompose = () => openModal('COMPOSE');
const onOpenCompose = () => {
if (!userTouching.matches) return window.open('/compose', 'targetWindow', 'height=500,width=700');
openModal('COMPOSE');
};
return (
<Button

View file

@ -2,7 +2,7 @@ import React from 'react';
import { FormattedMessage } from 'react-intl';
import Text from 'pl-fe/components/ui/text';
import emojify from 'pl-fe/features/emoji';
import Emojify from 'pl-fe/features/emoji/emojify';
import { usePlFeConfig } from 'pl-fe/hooks/usePlFeConfig';
import sourceCode from 'pl-fe/utils/code';
@ -12,10 +12,9 @@ const LinkFooter: React.FC = (): JSX.Element => {
return (
<Text theme='muted' size='sm'>
{plFeConfig.linkFooterMessage ? (
<span
className='inline-block align-middle'
dangerouslySetInnerHTML={{ __html: emojify(plFeConfig.linkFooterMessage) }}
/>
<span className='inline-block align-middle'>
<Emojify text={plFeConfig.linkFooterMessage} />
</span>
) : (
<FormattedMessage
id='getting_started.open_source_notice'

View file

@ -9,6 +9,7 @@ import Modal from 'pl-fe/components/ui/modal';
import Spinner from 'pl-fe/components/ui/spinner';
import Stack from 'pl-fe/components/ui/stack';
import Text from 'pl-fe/components/ui/text';
import Emojify from 'pl-fe/features/emoji/emojify';
import { useAppDispatch } from 'pl-fe/hooks/useAppDispatch';
import { useAppSelector } from 'pl-fe/hooks/useAppSelector';
@ -42,8 +43,7 @@ const CompareHistoryModal: React.FC<BaseModalProps & CompareHistoryModalProps> =
body = (
<div className='divide-y divide-solid divide-gray-200 dark:divide-gray-800'>
{versions?.map((version) => {
const content = <ParsedContent html={version.contentHtml} mentions={status?.mentions} hasQuote={!!status?.quote_id} />;
const spoilerContent = { __html: version.spoilerHtml };
const content = <ParsedContent html={version.content} mentions={status?.mentions} hasQuote={!!status?.quote_id} emojis={version.emojis} />;
const poll = typeof version.poll !== 'string' && version.poll;
@ -51,7 +51,9 @@ const CompareHistoryModal: React.FC<BaseModalProps & CompareHistoryModalProps> =
<div className='flex flex-col py-2 first:pt-0 last:pb-0'>
{version.spoiler_text?.length > 0 && (
<>
<span dangerouslySetInnerHTML={spoilerContent} />
<span>
<Emojify text={version.spoiler_text} emojis={version.emojis} />
</span>
<hr />
</>
)}
@ -71,7 +73,9 @@ const CompareHistoryModal: React.FC<BaseModalProps & CompareHistoryModalProps> =
role='radio'
/>
<span dangerouslySetInnerHTML={{ __html: option.title_emojified }} />
<span>
<ParsedContent html={option.title} emojis={version.emojis} />
</span>
</HStack>
))}
</Stack>

View file

@ -324,7 +324,7 @@ const ComposeEventModal: React.FC<BaseModalProps & ComposeEventModalProps> = ({
onChange={onChangeHasEndTime}
/>
<Text tag='span' theme='muted'>
<FormattedMessage id='compose_event.fields.has_end_time' defaultMessage='The event has an end date' />
<FormattedMessage id='compose_event.fields.has_end_time' defaultMessage='This event has an end date' />
</Text>
</HStack>
{endTime && (

View file

@ -6,6 +6,7 @@ import ScrollableList from 'pl-fe/components/scrollable-list';
import Modal from 'pl-fe/components/ui/modal';
import Spinner from 'pl-fe/components/ui/spinner';
import AccountContainer from 'pl-fe/containers/account-container';
import Emojify from 'pl-fe/features/emoji/emojify';
import { useAppSelector } from 'pl-fe/hooks/useAppSelector';
import { makeGetAccount } from 'pl-fe/selectors';
@ -31,7 +32,13 @@ const FamiliarFollowersModal: React.FC<BaseModalProps & FamiliarFollowersModalPr
if (!account || !familiarFollowerIds) {
body = <Spinner />;
} else {
const emptyMessage = <FormattedMessage id='account.familiar_followers.empty' defaultMessage='No one you know follows {name}.' values={{ name: <span dangerouslySetInnerHTML={{ __html: account.display_name_html }} /> }} />;
const emptyMessage = (
<FormattedMessage
id='account.familiar_followers.empty'
defaultMessage='No one you know follows {name}.'
values={{ name: <span><Emojify text={account.display_name} emojis={account.emojis} /></span> }}
/>
);
body = (
<ScrollableList
@ -54,7 +61,7 @@ const FamiliarFollowersModal: React.FC<BaseModalProps & FamiliarFollowersModalPr
<FormattedMessage
id='column.familiar_followers'
defaultMessage='People you know following {name}'
values={{ name: <span dangerouslySetInnerHTML={{ __html: account?.display_name_html || '' }} /> }}
values={{ name: !!account && <span><Emojify text={account.display_name} emojis={account.emojis} /></span> }}
/>
}
onClose={onClickClose}

View file

@ -102,8 +102,8 @@ const HotkeysModal: React.FC<BaseModalProps> = ({ onClose }) => {
label: <FormattedMessage id='keyboard_shortcuts.back' defaultMessage='to navigate back' />,
},
isLoggedIn && {
key: <><Hotkey>s</Hotkey>, <Hotkey>/</Hotkey></>,
label: <FormattedMessage id='keyboard_shortcuts.search' defaultMessage='to focus search' />,
key: <><Hotkey>g</Hotkey> + <Hotkey>s</Hotkey></>,
label: <FormattedMessage id='keyboard_shortcuts.search' defaultMessage='to open search page' />,
},
{
key: <Hotkey>esc</Hotkey>,

View file

@ -64,7 +64,7 @@ const ConfirmationStep: React.FC<IConfirmationStep> = ({ group }) => {
size='md'
className='mx-auto max-w-sm [&_a]:text-primary-600 [&_a]:hover:underline [&_a]:dark:text-accent-blue'
>
<ParsedContent html={group.note_emojified} />
<ParsedContent html={group.note} emojis={group.emojis} />
</Text>
</Stack>
</Stack>

View file

@ -5,6 +5,7 @@ import { FormattedMessage } from 'react-intl';
import { fetchPinnedAccounts } from 'pl-fe/actions/accounts';
import Widget from 'pl-fe/components/ui/widget';
import AccountContainer from 'pl-fe/containers/account-container';
import Emojify from 'pl-fe/features/emoji/emojify';
import { WhoToFollowPanel } from 'pl-fe/features/ui/util/async-components';
import { useAppDispatch } from 'pl-fe/hooks/useAppDispatch';
import { useAppSelector } from 'pl-fe/hooks/useAppSelector';
@ -12,7 +13,7 @@ import { useAppSelector } from 'pl-fe/hooks/useAppSelector';
import type { Account } from 'pl-fe/normalizers/account';
interface IPinnedAccountsPanel {
account: Pick<Account, 'id' | 'display_name_html'>;
account: Pick<Account, 'id' | 'display_name' | 'emojis'>;
limit: number;
}
@ -36,7 +37,7 @@ const PinnedAccountsPanel: React.FC<IPinnedAccountsPanel> = ({ account, limit })
id='pinned_accounts.title'
defaultMessage='{name}s choices'
values={{
name: <span dangerouslySetInnerHTML={{ __html: account.display_name_html }} />,
name: <span><Emojify text={account.display_name} emojis={account.emojis} /></span>,
}}
/>}
>

View file

@ -8,7 +8,7 @@ import ProfileField from '../profile-field';
import type { Account } from 'pl-fe/normalizers/account';
interface IProfileFieldsPanel {
account: Pick<Account, 'fields'>;
account: Pick<Account, 'emojis' | 'fields'>;
}
/** Custom profile fields for sidebar. */
@ -16,7 +16,7 @@ const ProfileFieldsPanel: React.FC<IProfileFieldsPanel> = ({ account }) => (
<Widget>
<Stack space={4}>
{account.fields.map((field, i) => (
<ProfileField field={field} key={i} />
<ProfileField field={field} key={i} emojis={account.emojis} />
))}
</Stack>
</Widget>

View file

@ -10,6 +10,7 @@ import HStack from 'pl-fe/components/ui/hstack';
import Icon from 'pl-fe/components/ui/icon';
import Stack from 'pl-fe/components/ui/stack';
import Text from 'pl-fe/components/ui/text';
import Emojify from 'pl-fe/features/emoji/emojify';
import { useAppSelector } from 'pl-fe/hooks/useAppSelector';
import { usePlFeConfig } from 'pl-fe/hooks/usePlFeConfig';
import { capitalize } from 'pl-fe/utils/strings';
@ -122,8 +123,6 @@ const ProfileInfoPanel: React.FC<IProfileInfoPanel> = ({ account, username }) =>
);
}
const deactivated = account.deactivated ?? false;
const displayNameHtml = deactivated ? { __html: intl.formatMessage(messages.deactivated) } : { __html: account.display_name_html };
const memberSinceDate = intl.formatDate(account.created_at, { month: 'long', year: 'numeric' });
const badges = getBadges();
@ -132,7 +131,11 @@ const ProfileInfoPanel: React.FC<IProfileInfoPanel> = ({ account, username }) =>
<Stack space={2}>
<Stack>
<HStack space={1} alignItems='center'>
<Text size='lg' weight='bold' dangerouslySetInnerHTML={displayNameHtml} truncate />
<Text size='lg' weight='bold' truncate>
{account.deactivated
? <FormattedMessage id='account.deactivated' defaultMessage='Deactivated' />
: <Emojify text={account.display_name} emojis={account.emojis} />}
</Text>
{account.bot && <Badge slug='bot' title={intl.formatMessage(messages.bot)} />}
@ -160,9 +163,9 @@ const ProfileInfoPanel: React.FC<IProfileInfoPanel> = ({ account, username }) =>
<ProfileStats account={account} />
{!!account.note_emojified && (
{!!account.note && (
<Markup size='sm'>
<ParsedContent html={account.note_emojified} />
<ParsedContent html={account.note} emojis={account.emojis} />
</Markup>
)}
@ -208,7 +211,7 @@ const ProfileInfoPanel: React.FC<IProfileInfoPanel> = ({ account, username }) =>
{account.fields.length > 0 && (
<Stack space={2} className='mt-4 xl:hidden'>
{account.fields.map((field, i) => (
<ProfileField field={field} key={i} />
<ProfileField field={field} key={i} emojis={account.emojis} />
))}
</Stack>
)}

View file

@ -9,6 +9,7 @@ import HStack from 'pl-fe/components/ui/hstack';
import Stack from 'pl-fe/components/ui/stack';
import Text from 'pl-fe/components/ui/text';
import VerificationBadge from 'pl-fe/components/verification-badge';
import Emojify from 'pl-fe/features/emoji/emojify';
import { useAppSelector } from 'pl-fe/hooks/useAppSelector';
import { useSettings } from 'pl-fe/hooks/useSettings';
import { getAcct } from 'pl-fe/utils/accounts';
@ -29,7 +30,6 @@ const UserPanel: React.FC<IUserPanel> = ({ accountId, action, badges, domain })
const fqn = useAppSelector((state) => displayFqn(state));
if (!account) return null;
const displayNameHtml = { __html: account.display_name_html };
const acct = !account.acct.includes('@') && domain ? `${account.acct}@${domain}` : account.acct;
const header = account.header;
const verified = account.verified;
@ -67,7 +67,9 @@ const UserPanel: React.FC<IUserPanel> = ({ accountId, action, badges, domain })
<Stack>
<Link to={`/@${account.acct}`}>
<HStack space={1} alignItems='center'>
<Text size='lg' weight='bold' dangerouslySetInnerHTML={displayNameHtml} truncate />
<Text size='lg' weight='bold' truncate>
<Emojify text={account.display_name} emojis={account.emojis} />
</Text>
{verified && <VerificationBadge />}

View file

@ -4,7 +4,7 @@ import React from 'react';
import PollOption from 'pl-fe/components/polls/poll-option';
import Stack from 'pl-fe/components/ui/stack';
import type { Poll } from 'pl-fe/normalizers/poll';
import type { Poll } from 'pl-api';
interface IPollPreview {
poll: Poll;

View file

@ -9,6 +9,7 @@ import HoverAccountWrapper from 'pl-fe/components/hover-account-wrapper';
import HStack from 'pl-fe/components/ui/hstack';
import Text from 'pl-fe/components/ui/text';
import VerificationBadge from 'pl-fe/components/verification-badge';
import Emojify from 'pl-fe/features/emoji/emojify';
import { useAppDispatch } from 'pl-fe/hooks/useAppDispatch';
import { useAppSelector } from 'pl-fe/hooks/useAppSelector';
import { useFeatures } from 'pl-fe/hooks/useFeatures';
@ -51,12 +52,9 @@ const ProfileFamiliarFollowers: React.FC<IProfileFamiliarFollowers> = ({ account
<HoverAccountWrapper accountId={account.id} key={account.id} element='span'>
<Link className='mention inline-block' to={`/@${account.acct}`}>
<HStack space={1} alignItems='center' grow>
<Text
size='sm'
theme='primary'
truncate
dangerouslySetInnerHTML={{ __html: account.display_name_html }}
/>
<Text size='sm' theme='primary' truncate>
<Emojify text={account.display_name} emojis={account.emojis} />
</Text>
{account.verified && <VerificationBadge />}
</HStack>

View file

@ -3,9 +3,12 @@ import React from 'react';
import { defineMessages, useIntl, FormatDateOptions } from 'react-intl';
import Markup from 'pl-fe/components/markup';
import { ParsedContent } from 'pl-fe/components/parsed-content';
import HStack from 'pl-fe/components/ui/hstack';
import Icon from 'pl-fe/components/ui/icon';
import Emojify from 'pl-fe/features/emoji/emojify';
import { CryptoAddress, LightningAddress } from 'pl-fe/features/ui/util/async-components';
import { unescapeHTML } from 'pl-fe/utils/html';
import type { Account } from 'pl-fe/normalizers/account';
@ -28,32 +31,35 @@ const dateFormatOptions: FormatDateOptions = {
interface IProfileField {
field: Account['fields'][number];
emojis?: Account['emojis'];
}
/** Renders a single profile field. */
const ProfileField: React.FC<IProfileField> = ({ field }) => {
const ProfileField: React.FC<IProfileField> = ({ field, emojis }) => {
const intl = useIntl();
if (isTicker(field.name)) {
return (
<CryptoAddress
ticker={getTicker(field.name).toLowerCase()}
address={field.value_plain}
address={unescapeHTML(field.value)}
/>
);
} else if (isZapEmoji(field.name)) {
return <LightningAddress address={field.value_plain} />;
return <LightningAddress address={unescapeHTML(field.value)} />;
}
return (
<dl>
<dt title={field.name}>
<Markup weight='bold' tag='span' dangerouslySetInnerHTML={{ __html: field.name_emojified }} />
<Markup weight='bold' tag='span'>
<Emojify text={field.name} emojis={emojis} />
</Markup>
</dt>
<dd
className={clsx({ 'text-success-500': field.verified_at })}
title={field.value_plain}
title={unescapeHTML(field.value)}
>
<HStack space={2} alignItems='center'>
{field.verified_at && (
@ -62,7 +68,9 @@ const ProfileField: React.FC<IProfileField> = ({ field }) => {
</span>
)}
<Markup className='overflow-hidden break-words' tag='span' dangerouslySetInnerHTML={{ __html: field.value_emojified }} />
<Markup className='overflow-hidden break-words' tag='span'>
<ParsedContent html={field.value} emojis={emojis} />
</Markup>
</HStack>
</dd>
</dl>

View file

@ -83,7 +83,6 @@ import {
Backups,
MfaForm,
ChatIndex,
ChatWidget,
ServerInfo,
Dashboard,
ModerationLog,
@ -356,7 +355,6 @@ const UI: React.FC<IUI> = ({ children }) => {
const node = useRef<HTMLDivElement | null>(null);
const me = useAppSelector(state => state.me);
const { account } = useOwnAccount();
const features = useFeatures();
const vapidKey = useAppSelector(state => getVapidKey(state));
const { isDropdownMenuOpen } = useUiStore();
@ -481,13 +479,13 @@ const UI: React.FC<IUI> = ({ children }) => {
<SidebarMenu />
</Suspense>
{me && features.chats && (
{/* {me && features.chats && (
<div className='hidden xl:block'>
<Suspense fallback={<div className='fixed bottom-0 z-[99] flex h-16 w-96 animate-pulse flex-col rounded-t-lg bg-white shadow-3xl dark:bg-gray-900 ltr:right-5 rtl:left-5' />}>
<ChatWidget />
</Suspense>
</div>
)}
)} */}
<ThumbNavigation />

View file

@ -14,7 +14,7 @@ import type { LexicalEditor } from 'lexical';
const keyMap = {
help: '?',
new: 'n',
search: ['/', 's'],
search: ['/', 's', 'g s'],
forceNew: 'option+n',
reply: 'r',
favourite: 'f',

View file

@ -8,6 +8,7 @@ import { ScrollContext } from 'react-router-scroll-4';
import * as BuildConfig from 'pl-fe/build-config';
import LoadingScreen from 'pl-fe/components/loading-screen';
import SiteErrorBoundary from 'pl-fe/components/site-error-boundary';
import ComposePage from 'pl-fe/features/compose';
import { ModalRoot, OnboardingWizard } from 'pl-fe/features/ui/util/async-components';
import { useAppSelector } from 'pl-fe/hooks/useAppSelector';
import { useLoggedIn } from 'pl-fe/hooks/useLoggedIn';
@ -46,6 +47,8 @@ const PlFeMount = () => {
<Redirect exact from='/' to={redirectRootNoLogin} />
)}
<Route path='/compose' component={ComposePage} />
<Route
path='/embed/:statusId'
render={(props) => (

View file

@ -1,14 +1,7 @@
import clsx from 'clsx';
import React, { useRef } from 'react';
import { useIntl } from 'react-intl';
import { Link } from 'react-router-dom';
import React from 'react';
import { uploadCompose } from 'pl-fe/actions/compose';
import Avatar from 'pl-fe/components/ui/avatar';
import Card, { CardBody } from 'pl-fe/components/ui/card';
import HStack from 'pl-fe/components/ui/hstack';
import Layout from 'pl-fe/components/ui/layout';
import ComposeForm from 'pl-fe/features/compose/components/compose-form';
import LinkFooter from 'pl-fe/features/ui/components/link-footer';
import {
WhoToFollowPanel,
@ -19,12 +12,9 @@ import {
BirthdayPanel,
AnnouncementsPanel,
} from 'pl-fe/features/ui/util/async-components';
import { useAppDispatch } from 'pl-fe/hooks/useAppDispatch';
import { useAppSelector } from 'pl-fe/hooks/useAppSelector';
import { useDraggedFiles } from 'pl-fe/hooks/useDraggedFiles';
import { useFeatures } from 'pl-fe/hooks/useFeatures';
import { useIsMobile } from 'pl-fe/hooks/useIsMobile';
import { useOwnAccount } from 'pl-fe/hooks/useOwnAccount';
import { usePlFeConfig } from 'pl-fe/hooks/usePlFeConfig';
interface IHomeLayout {
@ -32,62 +22,18 @@ interface IHomeLayout {
}
const HomeLayout: React.FC<IHomeLayout> = ({ children }) => {
const intl = useIntl();
const dispatch = useAppDispatch();
const me = useAppSelector(state => state.me);
const { account } = useOwnAccount();
const features = useFeatures();
const plFeConfig = usePlFeConfig();
const composeId = 'home';
const composeBlock = useRef<HTMLDivElement>(null);
const isMobile = useIsMobile();
const hasCrypto = typeof plFeConfig.cryptoAddresses.getIn([0, 'ticker']) === 'string';
const cryptoLimit = plFeConfig.cryptoDonatePanel.get('limit', 0);
const { isDragging, isDraggedOver } = useDraggedFiles(composeBlock, (files) => {
dispatch(uploadCompose(composeId, files, intl));
});
const acct = account ? account.acct : '';
const avatar = account ? account.avatar : '';
return (
<>
<Layout.Main className={clsx('black:space-y-0 dark:divide-gray-800', { 'pt-3 sm:pt-0 space-y-3': !isMobile })}>
{me && (
<Card
className={clsx('relative z-[1] border-gray-200 transition black:border-b black:border-gray-800 dark:border-gray-800', {
'border-2 border-primary-600 border-dashed z-[99]': isDragging,
'ring-2 ring-offset-2 ring-primary-600': isDraggedOver,
'border-b': isMobile,
})}
variant='rounded'
ref={composeBlock}
>
<CardBody>
<HStack alignItems='start' space={2}>
<Link to={`/@${acct}`}>
<Avatar src={avatar} alt={account?.avatar_description} size={42} />
</Link>
<div className='w-full translate-y-0.5'>
<ComposeForm
id={composeId}
shouldCondense
autoFocus={false}
clickableAreaRef={composeBlock}
withAvatar
transparent
/>
</div>
</HStack>
</CardBody>
</Card>
)}
{children}
</Layout.Main>

View file

@ -1538,12 +1538,10 @@
"status.reply_all": "",
"status.report": "Κατάγγειλε @{name}",
"status.sensitive_warning": "Ευαίσθητο περιεχόμενο",
"status.sensitive_warning.subtitle": "This content may not be suitable for all audiences.",
"status.share": "Μοιράσου",
"status.show_filter_reason": "",
"status.show_less_all": "Δείξε λιγότερα για όλα",
"status.show_more_all": "Δείξε περισσότερα για όλα",
"status.show_original": "Show original",
"status.spoiler.collapse": "",
"status.spoiler.expand": "",
"status.title": "Post",

View file

@ -1538,12 +1538,10 @@
"status.reply_all": "",
"status.report": "𐑮𐑦𐑐𐑹𐑑 @{name}",
"status.sensitive_warning": "𐑕𐑧𐑯𐑕𐑦𐑑𐑦𐑝 𐑒𐑪𐑯𐑑𐑧𐑯𐑑",
"status.sensitive_warning.subtitle": "This content may not be suitable for all audiences.",
"status.share": "𐑖𐑺",
"status.show_filter_reason": "",
"status.show_less_all": "𐑖𐑴 𐑤𐑧𐑕 𐑓 𐑷𐑤",
"status.show_more_all": "𐑖𐑴 𐑥𐑹 𐑓 𐑷𐑤",
"status.show_original": "Show original",
"status.spoiler.collapse": "",
"status.spoiler.expand": "",
"status.title": "𐑐𐑴𐑕𐑑",

View file

@ -439,7 +439,7 @@
"compose_event.fields.description_placeholder": "Description",
"compose_event.fields.end_time_label": "Event end date",
"compose_event.fields.end_time_placeholder": "Event ends on…",
"compose_event.fields.has_end_time": "The event has an end date",
"compose_event.fields.has_end_time": "This event has an end date",
"compose_event.fields.location_label": "Event location",
"compose_event.fields.name_label": "Event name",
"compose_event.fields.name_placeholder": "Name",
@ -879,16 +879,20 @@
"icon_button.icons": "Icons",
"icon_button.label": "Select icon",
"import_data.actions.import": "Import",
"import_data.actions.import_archive": "Import archive",
"import_data.actions.import_blocks": "Import blocks",
"import_data.actions.import_follows": "Import follows",
"import_data.actions.import_mutes": "Import mutes",
"import_data.archive_label": "Archive",
"import_data.blocks_label": "Blocks",
"import_data.follows_label": "Follows",
"import_data.hints.archive": "Archive containing an archive of statuses",
"import_data.hints.blocks": "CSV file containing a list of blocked accounts",
"import_data.hints.follows": "CSV file containing a list of followed accounts",
"import_data.hints.mutes": "CSV file containing a list of muted accounts",
"import_data.mutes_label": "Mutes",
"import_data.overwrite": "Overwrite instead of appending",
"import_data.success.archive": "Archive imported successfully",
"import_data.success.blocks": "Blocks imported successfully",
"import_data.success.followers": "Followers imported successfully",
"import_data.success.mutes": "Mutes imported successfully",
@ -958,7 +962,7 @@
"keyboard_shortcuts.react": "to react",
"keyboard_shortcuts.reply": "to reply",
"keyboard_shortcuts.requests": "to open follow requests list",
"keyboard_shortcuts.search": "to focus search",
"keyboard_shortcuts.search": "to open search page",
"keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW",
"keyboard_shortcuts.toggle_sensitivity": "to show/hide media",
"keyboard_shortcuts.toot": "to start a new post",
@ -1061,7 +1065,6 @@
"mute_modal.auto_expire": "Automatically expire mute?",
"mute_modal.duration": "Duration",
"mute_modal.hide_notifications": "Hide notifications from this user?",
"navigation.chats": "Chats",
"navigation.compose": "Compose",
"navigation.compose_group": "Compose to group",
"navigation.dashboard": "Dashboard",
@ -1256,6 +1259,7 @@
"preferences.fields.theme": "Theme",
"preferences.fields.underline_links_label": "Always underline links in posts",
"preferences.fields.unfollow_modal_label": "Show confirmation dialog before unfollowing someone",
"preferences.fields.wrench_label": "Display wrench reaction button",
"preferences.hints.demetricator": "Decrease social media anxiety by hiding all numbers from the site.",
"preferences.notifications.advanced": "Show all notification categories",
"preferences.options.content_type_html": "HTML",
@ -1433,6 +1437,7 @@
"status.add_known_language": "Do not auto-translate posts in {language}.",
"status.admin_account": "Moderate @{name}",
"status.admin_status": "Open this post in the moderation interface",
"status.application_name": "Sent from {name}",
"status.approval.pending": "Pending approval",
"status.approval.rejected": "Rejected",
"status.bookmark": "Bookmark",
@ -1493,7 +1498,6 @@
"status.show_filter_reason": "Show anyway",
"status.show_less_all": "Show less for all",
"status.show_more_all": "Show more for all",
"status.show_original": "Show original",
"status.spoiler.collapse": "Collapse",
"status.spoiler.expand": "Expand",
"status.title": "Post details",
@ -1512,6 +1516,7 @@
"status.visibility.local": "The post is only visible to users on your instance",
"status.visibility.mutuals_only": "The post is only visible to people who mutually follow the author",
"status.visibility.private": "The post is only visible to followers of the author",
"status.wrench": "Wrench reaction",
"status_list.queue_label": "Click to see {count} new {count, plural, one {post} other {posts}}",
"statuses.quote_tombstone": "Post is unavailable.",
"statuses.tombstone": "One or more posts are unavailable.",

View file

@ -1649,12 +1649,10 @@
"status.reply_all": "",
"status.report": "Reportar",
"status.sensitive_warning": "Contenido sensible",
"status.sensitive_warning.subtitle": "This content may not be suitable for all audiences.",
"status.share": "Compartir",
"status.show_filter_reason": "Mostrar de todos modos",
"status.show_less_all": "Mostrar menos para todo",
"status.show_more_all": "Mostrar más para todo",
"status.show_original": "Show original",
"status.spoiler.collapse": "",
"status.spoiler.expand": "",
"status.title": "Post",

View file

@ -1538,12 +1538,10 @@
"status.reply_all": "",
"status.report": "گزارش دادن @{name}",
"status.sensitive_warning": "محتوای حساس",
"status.sensitive_warning.subtitle": "This content may not be suitable for all audiences.",
"status.share": "هم‌رسانی",
"status.show_filter_reason": "",
"status.show_less_all": "نمایش کمتر همه",
"status.show_more_all": "نمایش بیشتر همه",
"status.show_original": "Show original",
"status.spoiler.collapse": "",
"status.spoiler.expand": "",
"status.title": "Post",

View file

@ -1567,12 +1567,10 @@
"status.reply_all": "",
"status.report": "Signaler @{name}",
"status.sensitive_warning": "Contenu sensible",
"status.sensitive_warning.subtitle": "This content may not be suitable for all audiences.",
"status.share": "Partager",
"status.show_filter_reason": "",
"status.show_less_all": "Tout replier",
"status.show_more_all": "Tout déplier",
"status.show_original": "Show original",
"status.spoiler.collapse": "",
"status.spoiler.expand": "",
"status.title": "Détail de la publication",

View file

@ -497,8 +497,6 @@
"compose_form.sensitive.marked": "",
"compose_form.sensitive.unmarked": "",
"compose_form.spoiler.marked": "Text is hidden behind warning",
"compose_form.spoiler.unmarked": "Text is not hidden",
"compose_form.spoiler_placeholder": "Write your warning here",
"compose_form.spoiler_remove": "Remove sensitive",
"compose_form.spoiler_title": "Sensitive content",
"confirmation_modal.cancel": "Cancel",
@ -1538,12 +1536,10 @@
"status.reply_all": "",
"status.report": "Report @{name}",
"status.sensitive_warning": "Sensitive content",
"status.sensitive_warning.subtitle": "This content may not be suitable for all audiences.",
"status.share": "Share",
"status.show_filter_reason": "",
"status.show_less_all": "Show less for all",
"status.show_more_all": "Show more for all",
"status.show_original": "Show original",
"status.spoiler.collapse": "",
"status.spoiler.expand": "",
"status.title": "Post",

View file

@ -1596,12 +1596,10 @@
"status.reply_all": "",
"status.report": "דיווח על @{name}",
"status.sensitive_warning": "תוכן רגיש",
"status.sensitive_warning.subtitle": "This content may not be suitable for all audiences.",
"status.share": "שיתוף",
"status.show_filter_reason": "",
"status.show_less_all": "הצג פחות לכולם",
"status.show_more_all": "הראה יותר לכולם",
"status.show_original": "Show original",
"status.spoiler.collapse": "",
"status.spoiler.expand": "",
"status.title": "פוסט",

View file

@ -497,8 +497,6 @@
"compose_form.sensitive.marked": "",
"compose_form.sensitive.unmarked": "",
"compose_form.spoiler.marked": "Text is hidden behind warning",
"compose_form.spoiler.unmarked": "Text is not hidden",
"compose_form.spoiler_placeholder": "Write your warning here",
"compose_form.spoiler_remove": "Remove sensitive",
"compose_form.spoiler_title": "Sensitive content",
"confirmation_modal.cancel": "Cancel",
@ -1538,12 +1536,10 @@
"status.reply_all": "",
"status.report": "Report @{name}",
"status.sensitive_warning": "Sensitive content",
"status.sensitive_warning.subtitle": "This content may not be suitable for all audiences.",
"status.share": "Share",
"status.show_filter_reason": "",
"status.show_less_all": "Show less for all",
"status.show_more_all": "Show more for all",
"status.show_original": "Show original",
"status.spoiler.collapse": "",
"status.spoiler.expand": "",
"status.title": "Post",

View file

@ -1561,12 +1561,10 @@
"status.reply_all": "",
"status.report": "Prijavi @{name}",
"status.sensitive_warning": "Osjetljiv sadržaj",
"status.sensitive_warning.subtitle": "This content may not be suitable for all audiences.",
"status.share": "Share",
"status.show_filter_reason": "",
"status.show_less_all": "Show less for all",
"status.show_more_all": "Prikaži više za sve",
"status.show_original": "Show original",
"status.spoiler.collapse": "",
"status.spoiler.expand": "",
"status.title": "Post",

View file

@ -1538,12 +1538,10 @@
"status.reply_all": "",
"status.report": "@{name} jelentése",
"status.sensitive_warning": "Szenzitív tartalom",
"status.sensitive_warning.subtitle": "This content may not be suitable for all audiences.",
"status.share": "Megosztás",
"status.show_filter_reason": "",
"status.show_less_all": "Kevesebbet mindenhol",
"status.show_more_all": "Többet mindenhol",
"status.show_original": "Show original",
"status.spoiler.collapse": "",
"status.spoiler.expand": "",
"status.title": "Post",

View file

@ -497,7 +497,6 @@
"compose_form.sensitive.marked": "",
"compose_form.sensitive.unmarked": "",
"compose_form.spoiler.marked": "Text is hidden behind warning",
"compose_form.spoiler.unmarked": "Text is not hidden",
"compose_form.spoiler_placeholder": "Averto di kontenajo",
"compose_form.spoiler_remove": "Remove sensitive",
"compose_form.spoiler_title": "Sensitive content",
@ -1538,12 +1537,10 @@
"status.reply_all": "",
"status.report": "Denuncar @{name}",
"status.sensitive_warning": "Trubliva kontenajo",
"status.sensitive_warning.subtitle": "This content may not be suitable for all audiences.",
"status.share": "Share",
"status.show_filter_reason": "",
"status.show_less_all": "Show less for all",
"status.show_more_all": "Show more for all",
"status.show_original": "Show original",
"status.spoiler.collapse": "",
"status.spoiler.expand": "",
"status.title": "Post",

View file

@ -1538,12 +1538,10 @@
"status.reply_all": "",
"status.report": "Kæra @{name}",
"status.sensitive_warning": "Viðkvæmt efni",
"status.sensitive_warning.subtitle": "This content may not be suitable for all audiences.",
"status.share": "Deila",
"status.show_filter_reason": "",
"status.show_less_all": "Sýna minna fyrir allt",
"status.show_more_all": "Sýna meira fyrir allt",
"status.show_original": "Show original",
"status.spoiler.collapse": "",
"status.spoiler.expand": "",
"status.title": "Færsla",

View file

@ -1538,12 +1538,10 @@
"status.reply_all": "",
"status.report": "Шағым @{name}",
"status.sensitive_warning": "Нәзік контент",
"status.sensitive_warning.subtitle": "This content may not be suitable for all audiences.",
"status.share": "Бөлісу",
"status.show_filter_reason": "",
"status.show_less_all": "Бәрін аздап көрсет",
"status.show_more_all": "Бәрін толығымен",
"status.show_original": "Show original",
"status.spoiler.collapse": "",
"status.spoiler.expand": "",
"status.title": "Post",

View file

@ -1538,12 +1538,10 @@
"status.reply_all": "",
"status.report": "신고",
"status.sensitive_warning": "민감한 미디어",
"status.sensitive_warning.subtitle": "This content may not be suitable for all audiences.",
"status.share": "공유",
"status.show_filter_reason": "",
"status.show_less_all": "모두 접기",
"status.show_more_all": "모두 펼치기",
"status.show_original": "Show original",
"status.spoiler.collapse": "",
"status.spoiler.expand": "",
"status.title": "Post",

View file

@ -497,8 +497,6 @@
"compose_form.sensitive.marked": "",
"compose_form.sensitive.unmarked": "",
"compose_form.spoiler.marked": "Text is hidden behind warning",
"compose_form.spoiler.unmarked": "Text is not hidden",
"compose_form.spoiler_placeholder": "Write your warning here",
"compose_form.spoiler_remove": "Remove sensitive",
"compose_form.spoiler_title": "Sensitive content",
"confirmation_modal.cancel": "Cancel",
@ -1538,12 +1536,10 @@
"status.reply_all": "",
"status.report": "Report @{name}",
"status.sensitive_warning": "Sensitive content",
"status.sensitive_warning.subtitle": "This content may not be suitable for all audiences.",
"status.share": "Share",
"status.show_filter_reason": "",
"status.show_less_all": "Show less for all",
"status.show_more_all": "Show more for all",
"status.show_original": "Show original",
"status.spoiler.collapse": "",
"status.spoiler.expand": "",
"status.title": "Post",

View file

@ -1538,12 +1538,10 @@
"status.reply_all": "",
"status.report": "Report @{name}",
"status.sensitive_warning": "Sensitive content",
"status.sensitive_warning.subtitle": "This content may not be suitable for all audiences.",
"status.share": "Share",
"status.show_filter_reason": "",
"status.show_less_all": "Show less for all",
"status.show_more_all": "Show more for all",
"status.show_original": "Show original",
"status.spoiler.collapse": "",
"status.spoiler.expand": "",
"status.title": "Post",

View file

@ -497,8 +497,6 @@
"compose_form.sensitive.marked": "",
"compose_form.sensitive.unmarked": "",
"compose_form.spoiler.marked": "Text is hidden behind warning",
"compose_form.spoiler.unmarked": "Text is not hidden",
"compose_form.spoiler_placeholder": "Write your warning here",
"compose_form.spoiler_remove": "Remove sensitive",
"compose_form.spoiler_title": "Sensitive content",
"confirmation_modal.cancel": "Cancel",
@ -1538,12 +1536,10 @@
"status.reply_all": "",
"status.report": "Report @{name}",
"status.sensitive_warning": "Sensitive content",
"status.sensitive_warning.subtitle": "This content may not be suitable for all audiences.",
"status.share": "Share",
"status.show_filter_reason": "",
"status.show_less_all": "Show less for all",
"status.show_more_all": "Show more for all",
"status.show_original": "Show original",
"status.spoiler.collapse": "",
"status.spoiler.expand": "",
"status.title": "Post",

View file

@ -497,8 +497,6 @@
"compose_form.sensitive.marked": "",
"compose_form.sensitive.unmarked": "",
"compose_form.spoiler.marked": "Text is hidden behind warning",
"compose_form.spoiler.unmarked": "Text is not hidden",
"compose_form.spoiler_placeholder": "Write your warning here",
"compose_form.spoiler_remove": "Remove sensitive",
"compose_form.spoiler_title": "Sensitive content",
"confirmation_modal.cancel": "Cancel",
@ -1538,12 +1536,10 @@
"status.reply_all": "",
"status.report": "Report @{name}",
"status.sensitive_warning": "Sensitive content",
"status.sensitive_warning.subtitle": "This content may not be suitable for all audiences.",
"status.share": "Share",
"status.show_filter_reason": "",
"status.show_less_all": "Show less for all",
"status.show_more_all": "Show more for all",
"status.show_original": "Show original",
"status.spoiler.collapse": "",
"status.spoiler.expand": "",
"status.title": "Post",

View file

@ -1538,12 +1538,10 @@
"status.reply_all": "",
"status.report": "Report @{name}",
"status.sensitive_warning": "Sensitive content",
"status.sensitive_warning.subtitle": "This content may not be suitable for all audiences.",
"status.share": "Share",
"status.show_filter_reason": "",
"status.show_less_all": "Show less for all",
"status.show_more_all": "Show more for all",
"status.show_original": "Show original",
"status.spoiler.collapse": "",
"status.spoiler.expand": "",
"status.title": "Post",

View file

@ -1538,12 +1538,10 @@
"status.reply_all": "",
"status.report": "Senhalar @{name}",
"status.sensitive_warning": "Contengut sensible",
"status.sensitive_warning.subtitle": "This content may not be suitable for all audiences.",
"status.share": "Partejar",
"status.show_filter_reason": "",
"status.show_less_all": "Los tornar plegar totes",
"status.show_more_all": "Los desplegar totes",
"status.show_original": "Show original",
"status.spoiler.collapse": "",
"status.spoiler.expand": "",
"status.title": "Post",

View file

@ -1104,7 +1104,7 @@
"navigation_bar.preferences": "Preferencje",
"navigation_bar.profile_directory": "Katalog profilów",
"new_event_panel.action": "Utwórz wydarzenie",
"new_event_panel.subtitle": "Nie możesz znaleźć tego, czeko szukasz? Utwórz własne wydarzenie.",
"new_event_panel.subtitle": "Nie możesz znaleźć tego, czego szukasz? Zaplanuj własne wydarzenie.",
"new_event_panel.title": "Utwórz nowe wydarzenie",
"new_group_panel.action": "Utwórz grupę",
"new_group_panel.subtitle": "Nie możesz znaleźć tego, czego szukasz? Utwórz własną prywatną lub publiczną grupę.",
@ -1112,15 +1112,15 @@
"notification.admin.report": "{name} zgłosił(a) {target}",
"notification.admin.sign_up": "{name} zarejestrował(a) się",
"notification.bite": "{name} ugryzł(a) Cię",
"notification.favourite": "{name} dodał(a) Twój wpis do ulubionych",
"notification.follow": "{name} zaczął(-ęła) Cię obserwować",
"notification.follow_request": "{name} poprosił(a) Cię o możliwość obserwacji",
"notification.favourite": "{name} {count, plural, one {dodał(a)} other {dodali}} Twój wpis do ulubionych",
"notification.follow": "{name} {count, plural, one {zaczął(-ęła)} other {zaczęli}} Cię obserwować",
"notification.follow_request": "{name} {count, plural, one {poprosił(a)} other {poprosili}} Cię o możliwość obserwacji",
"notification.mention": "{name} wspomniał(a) o tobie",
"notification.moderation_warning": "Otrzymałeś(-aś) ostrzeżenie moderacyjne",
"notification.more": "{count} więcej",
"notification.move": "{name} przeniósł(-osła) się na {targetName}",
"notification.pleroma:chat_mention": "{name} wysłał(a) Ci wiadomośść",
"notification.pleroma:emoji_reaction": "{name} zareagował(a) na Twój wpis",
"notification.more": "{count, plural, one {# inny użytkownik} other {# innych użytkowników}}",
"notification.move": "{name} {count, plural, one {przeniosł(a)} other {przenieśli}} się na {targetName}",
"notification.pleroma:chat_mention": "{name} {count, plural, one {wysłał(a)} other {wysłali}} Ci wiadomośść",
"notification.pleroma:emoji_reaction": "{name} {count, plural, one {zareagował(a)} other {zareagowali}} na Twój wpis",
"notification.pleroma:event_reminder": "Wydarzenie w którym bierzesz udział wkrótce się zaczyna",
"notification.pleroma:participation_accepted": "Twoje zgłoszenie udziału do wydarzenia zostało przyjęte",
"notification.pleroma:participation_request": "{name} chce wziąć udział w Twoim wydarzeniu",
@ -1130,6 +1130,7 @@
"notification.severed_relationships": "Utracono połączenia z {name}",
"notification.status": "{name} właśnie opublikował(a) wpis",
"notification.update": "{name} zedytował(a) wpis, który podbiłeś(-aś)",
"notification.user_approved": "Witamy w {instance}!",
"notifications.filter.all": "Wszystkie",
"notifications.filter.boosts": "Podbicia",
"notifications.filter.events": "Wydarzenia",
@ -1490,7 +1491,7 @@
"status.read_more": "Czytaj dalej",
"status.reblog": "Podbij",
"status.reblog_private": "Podbij dla odbiorców oryginalnego wpisu",
"status.reblogged_by": "{name} podbił(a)",
"status.reblogged_by": "{name} {count, plural, one {podbił(a)} other {podbili}}",
"status.reblogged_by_with_group": "{name} udostępnił(a) z {group}",
"status.reblogs.empty": "Nikt nie podbił jeszcze tego wpisu. Gdy ktoś to zrobi, pojawi się tutaj.",
"status.redraft": "Usuń i przeredaguj",

View file

@ -1538,12 +1538,10 @@
"status.reply_all": "",
"status.report": "Denunciar @{name}",
"status.sensitive_warning": "Conteúdo sensível",
"status.sensitive_warning.subtitle": "This content may not be suitable for all audiences.",
"status.share": "Compartilhar",
"status.show_filter_reason": "",
"status.show_less_all": "Mostrar menos para todas as mensagens",
"status.show_more_all": "Mostrar mais para todas as mensagens",
"status.show_original": "Show original",
"status.spoiler.collapse": "",
"status.spoiler.expand": "",
"status.title": "Post",

View file

@ -1538,12 +1538,10 @@
"status.reply_all": "",
"status.report": "Denunciar @{name}",
"status.sensitive_warning": "Conteúdo sensível",
"status.sensitive_warning.subtitle": "This content may not be suitable for all audiences.",
"status.share": "Partilhar",
"status.show_filter_reason": "",
"status.show_less_all": "Mostrar menos para todas",
"status.show_more_all": "Mostrar mais para todas",
"status.show_original": "Show original",
"status.spoiler.collapse": "",
"status.spoiler.expand": "",
"status.title": "Post",

View file

@ -1538,12 +1538,10 @@
"status.reply_all": "",
"status.report": "Raportează @{name}",
"status.sensitive_warning": "Conținut sensibil",
"status.sensitive_warning.subtitle": "This content may not be suitable for all audiences.",
"status.share": "Distribuie",
"status.show_filter_reason": "",
"status.show_less_all": "Arată mai puțin pentru toți",
"status.show_more_all": "Arată mai mult pentru toți",
"status.show_original": "Show original",
"status.spoiler.collapse": "",
"status.spoiler.expand": "",
"status.title": "Post",

Some files were not shown because too many files have changed in this diff Show more