mirror of
https://git.joinfirefish.org/firefish/firefish.git
synced 2024-05-18 20:11:10 +02:00
Merge branch 'develop' into refactor/check-hit-antenna
This commit is contained in:
commit
d513a6d170
|
@ -39,6 +39,9 @@ COPY packages/backend-rs packages/backend-rs/
|
|||
# Compile backend-rs
|
||||
RUN NODE_ENV='production' pnpm run --filter backend-rs build
|
||||
|
||||
# Copy/Overwrite index.js to mitigate the bug in napi-rs codegen
|
||||
COPY packages/backend-rs/index.js packages/backend-rs/built/index.js
|
||||
|
||||
# Copy in the rest of the files to compile
|
||||
COPY . ./
|
||||
RUN NODE_ENV='production' pnpm run --filter firefish-js build
|
||||
|
|
7
Makefile
7
Makefile
|
@ -3,7 +3,7 @@ export
|
|||
|
||||
|
||||
.PHONY: pre-commit
|
||||
pre-commit: format entities napi-index
|
||||
pre-commit: format entities napi
|
||||
|
||||
.PHONY: format
|
||||
format:
|
||||
|
@ -11,11 +11,12 @@ format:
|
|||
|
||||
.PHONY: entities
|
||||
entities:
|
||||
pnpm --filter=backend run build:debug
|
||||
pnpm run migrate
|
||||
$(MAKE) -C ./packages/backend-rs regenerate-entities
|
||||
|
||||
.PHONY: napi-index
|
||||
napi-index:
|
||||
.PHONY: napi
|
||||
napi:
|
||||
$(MAKE) -C ./packages/backend-rs update-index
|
||||
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
COMPOSE='docker compose'
|
||||
COMPOSE='podman-compose'
|
||||
POSTGRES_PASSWORD=password
|
||||
POSTGRES_USER=firefish
|
||||
POSTGRES_DB=firefish_db
|
||||
|
|
|
@ -31,7 +31,7 @@ You can refer to [local-installation.md](./local-installation.md) to install the
|
|||
1. Copy example config file
|
||||
```sh
|
||||
cp dev/config.example.env dev/config.env
|
||||
# If you use container runtime other than Docker, you need to modify the "COMPOSE" variable
|
||||
# If you use container runtime other than Podman, you need to modify the "COMPOSE" variable
|
||||
# vim dev/config.env
|
||||
```
|
||||
1. Create `.config/default.yml` with the following content
|
||||
|
|
|
@ -5,17 +5,20 @@ Critical security updates are indicated by the :warning: icon.
|
|||
- Server administrators should check [notice-for-admins.md](./notice-for-admins.md) as well.
|
||||
- Third-party client/bot developers may want to check [api-change.md](./api-change.md) as well.
|
||||
|
||||
## Unreleased
|
||||
## :warning: [v20240430](https://firefish.dev/firefish/firefish/-/merge_requests/10781/commits)
|
||||
|
||||
- Add ability to group similar notifications
|
||||
- Add features to share links to an account in the three dots menu on the profile page
|
||||
- Improve server logs
|
||||
- Fix bugs
|
||||
- Fix bugs (including a critical security issue)
|
||||
- We are very thankful to @tesaguri and Laura Hausmann for helping to fix the security issue.
|
||||
|
||||
## [v20240424](https://firefish.dev/firefish/firefish/-/merge_requests/10765/commits)
|
||||
|
||||
- Improve the usability of the feature to prevent forgetting to write alt texts
|
||||
- Add a server-wide setting for the maximum number of antennas each user can create
|
||||
- Fix bugs
|
||||
- Fix bugs (including a medium severity security issue)
|
||||
- We are very thankful to @mei23 for kindly sharing the information about the security issue.
|
||||
|
||||
## [v20240421](https://firefish.dev/firefish/firefish/-/merge_requests/10756/commits)
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ DELETE FROM "migrations" WHERE name IN (
|
|||
'DropUnusedUserprofileColumns1714259023878',
|
||||
'AntennaJsonbToArray1714192520471',
|
||||
'RenameType1714191375157',
|
||||
'DropUnusedIndexes1714643926317',
|
||||
'AlterAkaType1714099399879',
|
||||
'AddDriveFileUsage1713451569342',
|
||||
'ConvertCwVarcharToText1713225866247',
|
||||
|
@ -74,6 +75,22 @@ ALTER TABLE "emoji" RENAME COLUMN "mimeType" TO "type";
|
|||
ALTER TABLE "moderation_log" RENAME COLUMN "kind" TO "type";
|
||||
ALTER TABLE "notification" RENAME COLUMN "kind" TO "type";
|
||||
|
||||
-- drop-unused-indexes
|
||||
CREATE INDEX "IDX_01f4581f114e0ebd2bbb876f0b" ON "note_reaction" ("createdAt");
|
||||
CREATE INDEX "IDX_0610ebcfcfb4a18441a9bcdab2" ON "poll" ("userId");
|
||||
CREATE INDEX "IDX_25dfc71b0369b003a4cd434d0b" ON "note" ("attachedFileTypes");
|
||||
CREATE INDEX "IDX_2710a55f826ee236ea1a62698f" ON "hashtag" ("mentionedUsersCount");
|
||||
CREATE INDEX "IDX_4c02d38a976c3ae132228c6fce" ON "hashtag" ("mentionedRemoteUsersCount");
|
||||
CREATE INDEX "IDX_51c063b6a133a9cb87145450f5" ON "note" ("fileIds");
|
||||
CREATE INDEX "IDX_54ebcb6d27222913b908d56fd8" ON "note" ("mentions");
|
||||
CREATE INDEX "IDX_7fa20a12319c7f6dc3aed98c0a" ON "poll" ("userHost");
|
||||
CREATE INDEX "IDX_88937d94d7443d9a99a76fa5c0" ON "note" ("tags");
|
||||
CREATE INDEX "IDX_b11a5e627c41d4dc3170f1d370" ON "notification" ("createdAt");
|
||||
CREATE INDEX "IDX_c8dfad3b72196dd1d6b5db168a" ON "drive_file" ("createdAt");
|
||||
CREATE INDEX "IDX_d57f9030cd3af7f63ffb1c267c" ON "hashtag" ("attachedUsersCount");
|
||||
CREATE INDEX "IDX_e5848eac4940934e23dbc17581" ON "drive_file" ("uri");
|
||||
CREATE INDEX "IDX_fa99d777623947a5b05f394cae" ON "user" ("tags");
|
||||
|
||||
-- alter-aka-type
|
||||
ALTER TABLE "user" RENAME COLUMN "alsoKnownAs" TO "alsoKnownAsOld";
|
||||
ALTER TABLE "user" ADD COLUMN "alsoKnownAs" text;
|
||||
|
|
|
@ -269,7 +269,7 @@ In this instruction, we use [Caddy](https://caddyserver.com/) to make the Firefi
|
|||
WorkingDirectory=/home/firefish/firefish
|
||||
Environment="NODE_ENV=production"
|
||||
Environment="npm_config_cache=/tmp"
|
||||
Environment="NODE_OPTIONS=--max-old-space-size=3072"
|
||||
Environment="NODE_OPTIONS=--max-old-space-size=3072"
|
||||
# uncomment the following line if you use jemalloc (note that the path varies on different environments)
|
||||
# Environment="LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so.2"
|
||||
StandardOutput=journal
|
||||
|
|
|
@ -2,12 +2,33 @@
|
|||
|
||||
You can skip intermediate versions when upgrading from an old version, but please read the notices and follow the instructions for each intermediate version before [upgrading](./upgrade.md).
|
||||
|
||||
## Unreleased
|
||||
## v20240430
|
||||
|
||||
### For all users
|
||||
|
||||
You can control the verbosity of the server log by adding `maxLogLevel` in `.config/default.yml`. `logLevels` has been deprecated in favor of this setting. (see also: <https://firefish.dev/firefish/firefish/-/blob/eac0c1c47cd23789dcc395ab08b074934409fd96/.config/example.yml#L152>)
|
||||
|
||||
### For systemd/pm2 users
|
||||
|
||||
Not only Firefish but also Node.js has recently fixed a few security issues:
|
||||
|
||||
- https://nodejs.org/en/blog/vulnerability/april-2024-security-releases
|
||||
- https://nodejs.org/en/blog/vulnerability/april-2024-security-releases-2
|
||||
|
||||
So, it is highly recommended that you upgrade your Node.js version as well. The new versions are
|
||||
|
||||
- Node v18.20.2 (v18.x LTS)
|
||||
- Node v20.12.2 (v20.x LTS)
|
||||
- Node v21.7.3 (v21.x)
|
||||
|
||||
You can check your Node.js version by this command:
|
||||
|
||||
```sh
|
||||
node --version
|
||||
```
|
||||
|
||||
[Node v22](https://nodejs.org/en/blog/announcements/v22-release-announce) was also released several days ago, but we have not yet tested Firefish with this version.
|
||||
|
||||
## v20240413
|
||||
|
||||
### For all users
|
||||
|
|
|
@ -738,6 +738,7 @@ _notification:
|
|||
reacted: Ha reaccionat a la teva publicació
|
||||
renoted: Ha impulsat la teva publicació
|
||||
voted: Ha votat a la teva enquesta
|
||||
andCountUsers: I {count} usuaris més {acted}
|
||||
_deck:
|
||||
_columns:
|
||||
notifications: "Notificacions"
|
||||
|
@ -2292,3 +2293,11 @@ media: Multimèdia
|
|||
antennaLimit: El nombre màxim d'antenes que pot crear un usuari
|
||||
showAddFileDescriptionAtFirstPost: Obra de forma automàtica un formulari per escriure
|
||||
una descripció quant intentes publicar un fitxer que no en té
|
||||
remoteFollow: Seguiment remot
|
||||
cannotEditVisibility: No pots canviar la visibilitat
|
||||
useThisAccountConfirm: Vols continuar amb aquest compte?
|
||||
inputAccountId: Sisplau introdueix el teu compte (per exemple @firefish@info.firefish.dev)
|
||||
getQrCode: Mostrar el codi QR
|
||||
copyRemoteFollowUrl: Còpia la adreça URL del seguidor remot
|
||||
foldNotification: Agrupar les notificacions similars
|
||||
slashQuote: Cita encadenada
|
||||
|
|
|
@ -2239,4 +2239,5 @@ autocorrectNoteLanguage: "Show a warning if the post language does not match the
|
|||
incorrectLanguageWarning: "It looks like your post is in {detected}, but you selected
|
||||
{current}.\nWould you like to set the language to {detected} instead?"
|
||||
noteEditHistory: "Post edit history"
|
||||
slashQuote: "Chain quote"
|
||||
foldNotification: "Group similar notifications"
|
||||
|
|
|
@ -928,7 +928,7 @@ colored: "Coloré"
|
|||
label: "Étiquette"
|
||||
localOnly: "Local seulement"
|
||||
account: "Comptes"
|
||||
getQrCode: "Obtenir le code QR"
|
||||
getQrCode: "Afficher le code QR"
|
||||
|
||||
_emailUnavailable:
|
||||
used: "Adresse non disponible"
|
||||
|
@ -1836,6 +1836,7 @@ _notification:
|
|||
reacted: a réagit à votre publication
|
||||
renoted: a boosté votre publication
|
||||
voted: a voté pour votre sondage
|
||||
andCountUsers: et {count} utilisateur(s) de plus {acted}
|
||||
_deck:
|
||||
alwaysShowMainColumn: "Toujours afficher la colonne principale"
|
||||
columnAlign: "Aligner les colonnes"
|
||||
|
@ -2321,3 +2322,13 @@ markLocalFilesNsfwByDefaultDescription: Indépendamment de ce réglage, les util
|
|||
ne sont pas affectés.
|
||||
noteEditHistory: Historique des publications
|
||||
media: Multimédia
|
||||
antennaLimit: Le nombre maximal d'antennes que chaque utilisateur peut créer
|
||||
showAddFileDescriptionAtFirstPost: Ouvrez automatiquement un formulaire pour écrire
|
||||
une description lorsque vous tentez de publier des fichiers sans description
|
||||
foldNotification: Grouper les notifications similaires
|
||||
cannotEditVisibility: Vous ne pouvez pas modifier la visibilité
|
||||
useThisAccountConfirm: Voulez-vous continuer avec ce compte ?
|
||||
inputAccountId: Veuillez saisir votre compte (par exemple, @firefish@info.firefish.dev)
|
||||
remoteFollow: Abonnement à distance
|
||||
copyRemoteFollowUrl: Copier l'URL d'abonnement à distance
|
||||
slashQuote: Citation enchaînée
|
||||
|
|
|
@ -2278,3 +2278,4 @@ cannotEditVisibility: Kamu tidak bisa menyunting keterlihatan
|
|||
useThisAccountConfirm: Apakah kamu ingin melanjutkan dengan akun ini?
|
||||
inputAccountId: Silakan memasukkan akunmu (misalnya, @firefish@info.firefish.dev)
|
||||
copyRemoteFollowUrl: Salin URL ikuti jarak jauh
|
||||
slashQuote: Kutipan rantai
|
||||
|
|
|
@ -1902,7 +1902,7 @@ _notification:
|
|||
reacted: がリアクションしました
|
||||
renoted: がブーストしました
|
||||
voted: が投票しました
|
||||
andCountUsers: と{count}人が{acted}しました
|
||||
andCountUsers: と{count}人{acted}
|
||||
_deck:
|
||||
alwaysShowMainColumn: "常にメインカラムを表示"
|
||||
columnAlign: "カラムの寄せ"
|
||||
|
@ -2067,3 +2067,4 @@ useThisAccountConfirm: このアカウントで操作を続けますか?
|
|||
getQrCode: QRコードを表示
|
||||
copyRemoteFollowUrl: リモートからフォローするURLをコピー
|
||||
foldNotification: 同じ種類の通知をまとめて表示する
|
||||
slashQuote: 繋げて引用
|
||||
|
|
|
@ -2066,4 +2066,5 @@ autocorrectNoteLanguage: 当帖子语言不符合自动检测的结果的时候
|
|||
incorrectLanguageWarning: "看上去您帖子使用的语言是{detected},但您选择的语言是{current}。\n要改为以{detected}发帖吗?"
|
||||
noteEditHistory: "帖子编辑历史"
|
||||
media: 媒体
|
||||
slashQuote: "斜杠引用"
|
||||
foldNotification: "将通知按同类型分组"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "firefish",
|
||||
"version": "20240424",
|
||||
"version": "20240430",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://firefish.dev/firefish/firefish.git"
|
||||
|
|
|
@ -19,6 +19,13 @@ export function fromHtml(html: string, hashtagNames?: string[]): string {
|
|||
return appendChildren(childNodes, background).join("").trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* We only exclude text containing asterisks, since the other marks can almost be considered intentionally used.
|
||||
*/
|
||||
function escapeAmbiguousMfmMarks(text: string) {
|
||||
return text.includes("*") ? `<plain>${text}</plain>` : text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get only the text, ignoring all formatting inside
|
||||
* @param node
|
||||
|
@ -62,7 +69,7 @@ export function fromHtml(html: string, hashtagNames?: string[]): string {
|
|||
background = "",
|
||||
): (string | string[])[] {
|
||||
if (treeAdapter.isTextNode(node)) {
|
||||
return [node.value];
|
||||
return [escapeAmbiguousMfmMarks(node.value)];
|
||||
}
|
||||
|
||||
// Skip comment or document type node
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
import type { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class DropUnusedIndexes1714643926317 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`DROP INDEX "IDX_01f4581f114e0ebd2bbb876f0b"`);
|
||||
await queryRunner.query(`DROP INDEX "IDX_0610ebcfcfb4a18441a9bcdab2"`);
|
||||
await queryRunner.query(`DROP INDEX "IDX_25dfc71b0369b003a4cd434d0b"`);
|
||||
await queryRunner.query(`DROP INDEX "IDX_2710a55f826ee236ea1a62698f"`);
|
||||
await queryRunner.query(`DROP INDEX "IDX_4c02d38a976c3ae132228c6fce"`);
|
||||
await queryRunner.query(`DROP INDEX "IDX_51c063b6a133a9cb87145450f5"`);
|
||||
await queryRunner.query(`DROP INDEX "IDX_54ebcb6d27222913b908d56fd8"`);
|
||||
await queryRunner.query(`DROP INDEX "IDX_7fa20a12319c7f6dc3aed98c0a"`);
|
||||
await queryRunner.query(`DROP INDEX "IDX_88937d94d7443d9a99a76fa5c0"`);
|
||||
await queryRunner.query(`DROP INDEX "IDX_b11a5e627c41d4dc3170f1d370"`);
|
||||
await queryRunner.query(`DROP INDEX "IDX_c8dfad3b72196dd1d6b5db168a"`);
|
||||
await queryRunner.query(`DROP INDEX "IDX_d57f9030cd3af7f63ffb1c267c"`);
|
||||
await queryRunner.query(`DROP INDEX "IDX_e5848eac4940934e23dbc17581"`);
|
||||
await queryRunner.query(`DROP INDEX "IDX_fa99d777623947a5b05f394cae"`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_01f4581f114e0ebd2bbb876f0b" ON "note_reaction" ("createdAt")`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_0610ebcfcfb4a18441a9bcdab2" ON "poll" ("userId")`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_25dfc71b0369b003a4cd434d0b" ON "note" ("attachedFileTypes")`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_2710a55f826ee236ea1a62698f" ON "hashtag" ("mentionedUsersCount")`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_4c02d38a976c3ae132228c6fce" ON "hashtag" ("mentionedRemoteUsersCount")`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_51c063b6a133a9cb87145450f5" ON "note" ("fileIds")`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_54ebcb6d27222913b908d56fd8" ON "note" ("mentions")`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_7fa20a12319c7f6dc3aed98c0a" ON "poll" ("userHost")`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_88937d94d7443d9a99a76fa5c0" ON "note" ("tags")`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_b11a5e627c41d4dc3170f1d370" ON "notification" ("createdAt")`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_c8dfad3b72196dd1d6b5db168a" ON "drive_file" ("createdAt")`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_d57f9030cd3af7f63ffb1c267c" ON "hashtag" ("attachedUsersCount")`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_e5848eac4940934e23dbc17581" ON "drive_file" ("uri")`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_fa99d777623947a5b05f394cae" ON "user" ("tags")`,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -23,7 +23,6 @@ export class DriveFile {
|
|||
@PrimaryColumn(id())
|
||||
public id: string;
|
||||
|
||||
@Index()
|
||||
@Column("timestamp without time zone", {
|
||||
comment: "The created date of the DriveFile.",
|
||||
})
|
||||
|
@ -147,7 +146,6 @@ export class DriveFile {
|
|||
})
|
||||
public webpublicAccessKey: string | null;
|
||||
|
||||
@Index()
|
||||
@Column("varchar", {
|
||||
length: 512,
|
||||
nullable: true,
|
||||
|
|
|
@ -19,7 +19,6 @@ export class Hashtag {
|
|||
})
|
||||
public mentionedUserIds: User["id"][];
|
||||
|
||||
@Index()
|
||||
@Column("integer", {
|
||||
default: 0,
|
||||
})
|
||||
|
@ -43,7 +42,6 @@ export class Hashtag {
|
|||
})
|
||||
public mentionedRemoteUserIds: User["id"][];
|
||||
|
||||
@Index()
|
||||
@Column("integer", {
|
||||
default: 0,
|
||||
})
|
||||
|
@ -55,7 +53,6 @@ export class Hashtag {
|
|||
})
|
||||
public attachedUserIds: User["id"][];
|
||||
|
||||
@Index()
|
||||
@Column("integer", {
|
||||
default: 0,
|
||||
})
|
||||
|
|
|
@ -17,7 +17,6 @@ export class NoteReaction {
|
|||
@PrimaryColumn(id())
|
||||
public id: string;
|
||||
|
||||
@Index()
|
||||
@Column("timestamp without time zone", {
|
||||
comment: "The created date of the NoteReaction.",
|
||||
})
|
||||
|
|
|
@ -139,7 +139,6 @@ export class Note {
|
|||
|
||||
// FIXME: file id is not removed from this array even if the file is deleted
|
||||
// TODO: drop this column and use note_files
|
||||
@Index()
|
||||
@Column({
|
||||
...id(),
|
||||
array: true,
|
||||
|
@ -147,7 +146,6 @@ export class Note {
|
|||
})
|
||||
public fileIds: DriveFile["id"][];
|
||||
|
||||
@Index()
|
||||
@Column("varchar", {
|
||||
length: 256,
|
||||
array: true,
|
||||
|
@ -163,7 +161,6 @@ export class Note {
|
|||
})
|
||||
public visibleUserIds: User["id"][];
|
||||
|
||||
@Index()
|
||||
@Column({
|
||||
...id(),
|
||||
array: true,
|
||||
|
@ -184,7 +181,6 @@ export class Note {
|
|||
})
|
||||
public emojis: string[];
|
||||
|
||||
@Index()
|
||||
@Column("varchar", {
|
||||
length: 128,
|
||||
array: true,
|
||||
|
|
|
@ -20,7 +20,6 @@ export class Notification {
|
|||
@PrimaryColumn(id())
|
||||
public id: string;
|
||||
|
||||
@Index()
|
||||
@Column("timestamp without time zone", {
|
||||
comment: "The created date of the Notification.",
|
||||
})
|
||||
|
|
|
@ -44,14 +44,12 @@ export class Poll {
|
|||
})
|
||||
public noteVisibility: (typeof noteVisibilities)[number];
|
||||
|
||||
@Index()
|
||||
@Column({
|
||||
...id(),
|
||||
comment: "[Denormalized]",
|
||||
})
|
||||
public userId: User["id"];
|
||||
|
||||
@Index()
|
||||
@Column("varchar", {
|
||||
length: 512,
|
||||
nullable: true,
|
||||
|
|
|
@ -116,7 +116,6 @@ export class User {
|
|||
})
|
||||
public bannerId: DriveFile["id"] | null;
|
||||
|
||||
@Index()
|
||||
@Column("varchar", {
|
||||
length: 128,
|
||||
array: true,
|
||||
|
|
|
@ -24,7 +24,7 @@ const logger = new Logger("inbox");
|
|||
// Processing when an activity arrives in the user's inbox
|
||||
export default async (job: Bull.Job<InboxJobData>): Promise<string> => {
|
||||
const signature = job.data.signature; // HTTP-signature
|
||||
const activity = job.data.activity;
|
||||
let activity = job.data.activity;
|
||||
|
||||
//#region Log
|
||||
const info = Object.assign({}, activity) as any;
|
||||
|
@ -149,6 +149,8 @@ export default async (job: Bull.Job<InboxJobData>): Promise<string> => {
|
|||
return "skip: LD-Signatureの検証に失敗しました";
|
||||
}
|
||||
|
||||
activity = await ldSignature.compactToWellKnown(activity);
|
||||
|
||||
// もう一度actorチェック
|
||||
if (authUser.user.uri !== activity.actor) {
|
||||
return `skip: LD-Signature user(${authUser.user.uri}) !== activity.actor(${activity.actor})`;
|
||||
|
|
|
@ -518,6 +518,54 @@ const activitystreams = {
|
|||
},
|
||||
};
|
||||
|
||||
export const WellKnownContext = {
|
||||
"@context": [
|
||||
"https://www.w3.org/ns/activitystreams",
|
||||
"https://w3id.org/security/v1",
|
||||
{
|
||||
// as non-standards
|
||||
manuallyApprovesFollowers: "as:manuallyApprovesFollowers",
|
||||
movedTo: {
|
||||
"@id": "https://www.w3.org/ns/activitystreams#movedTo",
|
||||
"@type": "@id",
|
||||
},
|
||||
movedToUri: "as:movedTo",
|
||||
sensitive: "as:sensitive",
|
||||
Hashtag: "as:Hashtag",
|
||||
quoteUri: "fedibird:quoteUri",
|
||||
quoteUrl: "as:quoteUrl",
|
||||
// Mastodon
|
||||
toot: "http://joinmastodon.org/ns#",
|
||||
Emoji: "toot:Emoji",
|
||||
featured: "toot:featured",
|
||||
discoverable: "toot:discoverable",
|
||||
indexable: "toot:indexable",
|
||||
// schema
|
||||
schema: "http://schema.org#",
|
||||
PropertyValue: "schema:PropertyValue",
|
||||
value: "schema:value",
|
||||
// Firefish
|
||||
firefish: "https://firefish.dev/ns#",
|
||||
speakAsCat: "firefish:speakAsCat",
|
||||
// Misskey
|
||||
misskey: "https://misskey-hub.net/ns#",
|
||||
_misskey_talk: "misskey:_misskey_talk",
|
||||
_misskey_reaction: "misskey:_misskey_reaction",
|
||||
_misskey_votes: "misskey:_misskey_votes",
|
||||
_misskey_summary: "misskey:_misskey_summary",
|
||||
isCat: "misskey:isCat",
|
||||
// Fedibird
|
||||
fedibird: "http://fedibird.com/ns#",
|
||||
// vcard
|
||||
vcard: "http://www.w3.org/2006/vcard/ns#",
|
||||
// litepub
|
||||
litepub: "http://litepub.social/ns#",
|
||||
ChatMessage: "litepub:ChatMessage",
|
||||
directMessage: "litepub:directMessage",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const CONTEXTS: Record<string, unknown> = {
|
||||
"https://w3id.org/identity/v1": id_v1,
|
||||
"https://w3id.org/security/v1": security_v1,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import * as crypto from "node:crypto";
|
||||
import jsonld from "jsonld";
|
||||
import { CONTEXTS } from "./contexts.js";
|
||||
import { CONTEXTS, WellKnownContext } from "./contexts.js";
|
||||
import fetch from "node-fetch";
|
||||
import { httpAgent, httpsAgent } from "@/misc/fetch.js";
|
||||
|
||||
|
@ -89,6 +89,13 @@ export class LdSignature {
|
|||
});
|
||||
}
|
||||
|
||||
public async compactToWellKnown(data: any): Promise<any> {
|
||||
const options = { documentLoader: this.getLoader() };
|
||||
const context = WellKnownContext as any;
|
||||
delete data["signature"];
|
||||
return await jsonld.compact(data, context, options);
|
||||
}
|
||||
|
||||
private getLoader() {
|
||||
return async (url: string): Promise<any> => {
|
||||
if (!url.match("^https?://")) throw new Error(`Invalid URL ${url}`);
|
||||
|
|
|
@ -4,6 +4,7 @@ import { getUserKeypair } from "@/misc/keypair-store.js";
|
|||
import type { User } from "@/models/entities/user.js";
|
||||
import { LdSignature } from "../misc/ld-signature.js";
|
||||
import type { IActivity } from "../type.js";
|
||||
import { WellKnownContext } from "@/remote/activitypub/misc/contexts.js";
|
||||
|
||||
export const renderActivity = (x: any): IActivity | null => {
|
||||
if (x == null) return null;
|
||||
|
@ -12,52 +13,7 @@ export const renderActivity = (x: any): IActivity | null => {
|
|||
x.id = `${config.url}/${uuid()}`;
|
||||
}
|
||||
|
||||
return Object.assign(
|
||||
{
|
||||
"@context": [
|
||||
"https://www.w3.org/ns/activitystreams",
|
||||
"https://w3id.org/security/v1",
|
||||
{
|
||||
// as non-standards
|
||||
manuallyApprovesFollowers: "as:manuallyApprovesFollowers",
|
||||
movedToUri: "as:movedTo",
|
||||
sensitive: "as:sensitive",
|
||||
Hashtag: "as:Hashtag",
|
||||
quoteUri: "fedibird:quoteUri",
|
||||
quoteUrl: "as:quoteUrl",
|
||||
// Mastodon
|
||||
toot: "http://joinmastodon.org/ns#",
|
||||
Emoji: "toot:Emoji",
|
||||
featured: "toot:featured",
|
||||
discoverable: "toot:discoverable",
|
||||
indexable: "toot:indexable",
|
||||
// schema
|
||||
schema: "http://schema.org#",
|
||||
PropertyValue: "schema:PropertyValue",
|
||||
value: "schema:value",
|
||||
// Firefish
|
||||
firefish: "https://firefish.dev/ns#",
|
||||
speakAsCat: "firefish:speakAsCat",
|
||||
// Misskey
|
||||
misskey: "https://misskey-hub.net/ns#",
|
||||
_misskey_talk: "misskey:_misskey_talk",
|
||||
_misskey_reaction: "misskey:_misskey_reaction",
|
||||
_misskey_votes: "misskey:_misskey_votes",
|
||||
_misskey_summary: "misskey:_misskey_summary",
|
||||
isCat: "misskey:isCat",
|
||||
// Fedibird
|
||||
fedibird: "http://fedibird.com/ns#",
|
||||
// vcard
|
||||
vcard: "http://www.w3.org/2006/vcard/ns#",
|
||||
// ChatMessage
|
||||
litepub: "http://litepub.social/ns#",
|
||||
ChatMessage: "litepub:ChatMessage",
|
||||
directMessage: "litepub:directMessage",
|
||||
},
|
||||
],
|
||||
},
|
||||
x,
|
||||
);
|
||||
return Object.assign({}, WellKnownContext, x);
|
||||
};
|
||||
|
||||
export const attachLdSignature = async (
|
||||
|
|
|
@ -126,6 +126,7 @@ const contextmenu = computed((): MenuItem[] => {
|
|||
text: i18n.ts.copyLink,
|
||||
action: () => {
|
||||
copyToClipboard(pageUrl.value);
|
||||
os.success();
|
||||
},
|
||||
},
|
||||
];
|
||||
|
|
|
@ -215,7 +215,7 @@
|
|||
@click.stop="react()"
|
||||
>
|
||||
<i :class="icon('ph-smiley')"></i>
|
||||
<p class="count" v-if="reactionCount > 0 && hideEmojiViewer">{{reactionCount}}</p>
|
||||
<p v-if="reactionCount > 0 && hideEmojiViewer" class="count">{{reactionCount}}</p>
|
||||
</button>
|
||||
<button
|
||||
v-if="
|
||||
|
@ -228,7 +228,7 @@
|
|||
@click.stop="undoReact(appearNote)"
|
||||
>
|
||||
<i :class="icon('ph-minus')"></i>
|
||||
<p class="count" v-if="reactionCount > 0 && hideEmojiViewer">{{reactionCount}}</p>
|
||||
<p v-if="reactionCount > 0 && hideEmojiViewer" class="count">{{reactionCount}}</p>
|
||||
</button>
|
||||
<XQuoteButton class="button" :note="appearNote" />
|
||||
<button
|
||||
|
@ -546,6 +546,7 @@ function onContextmenu(ev: MouseEvent): void {
|
|||
text: i18n.ts.copyLink,
|
||||
action: () => {
|
||||
copyToClipboard(`${url}${notePage(appearNote.value)}`);
|
||||
os.success();
|
||||
},
|
||||
},
|
||||
appearNote.value.user.host != null
|
||||
|
|
|
@ -81,8 +81,8 @@
|
|||
</MkTab>
|
||||
|
||||
<MkPagination
|
||||
ref="repliesPagingComponent"
|
||||
v-if="tab === 'replies' && note.repliesCount > 0"
|
||||
ref="repliesPagingComponent"
|
||||
v-slot="{ items }"
|
||||
:pagination="repliesPagination"
|
||||
>
|
||||
|
|
|
@ -107,7 +107,7 @@
|
|||
@click.stop="react()"
|
||||
>
|
||||
<i :class="icon('ph-smiley')"></i>
|
||||
<p class="count" v-if="reactionCount > 0 && hideEmojiViewer">{{reactionCount}}</p>
|
||||
<p v-if="reactionCount > 0 && hideEmojiViewer" class="count">{{reactionCount}}</p>
|
||||
</button>
|
||||
<button
|
||||
v-if="
|
||||
|
@ -120,7 +120,7 @@
|
|||
@click.stop="undoReact(appearNote)"
|
||||
>
|
||||
<i :class="icon('ph-minus')"></i>
|
||||
<p class="count" v-if="reactionCount > 0 && hideEmojiViewer">{{reactionCount}}</p>
|
||||
<p v-if="reactionCount > 0 && hideEmojiViewer" class="count">{{reactionCount}}</p>
|
||||
</button>
|
||||
<XQuoteButton class="button" :note="appearNote" />
|
||||
<button
|
||||
|
@ -500,6 +500,7 @@ function onContextmenu(ev: MouseEvent): void {
|
|||
text: i18n.ts.copyLink,
|
||||
action: () => {
|
||||
copyToClipboard(`${url}${notePage(appearNote.value)}`);
|
||||
os.success();
|
||||
},
|
||||
},
|
||||
note.value.user.host != null
|
||||
|
|
|
@ -75,6 +75,7 @@
|
|||
import { computed, onMounted, onUnmounted, ref } from "vue";
|
||||
import type { Connection } from "firefish-js/src/streaming";
|
||||
import type { Channels } from "firefish-js/src/streaming.types";
|
||||
import type { entities } from "firefish-js";
|
||||
import XReactionIcon from "@/components/MkReactionIcon.vue";
|
||||
import XReactionTooltip from "@/components/MkReactionTooltip.vue";
|
||||
import { i18n } from "@/i18n";
|
||||
|
@ -89,7 +90,6 @@ import type {
|
|||
ReactionNotificationFolded,
|
||||
} from "@/types/notification";
|
||||
import XNote from "@/components/MkNote.vue";
|
||||
import type { entities } from "firefish-js";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
|
|
|
@ -17,8 +17,8 @@
|
|||
|
||||
<template #default="{ foldedItems: notifications }">
|
||||
<XList
|
||||
:items="notifications"
|
||||
v-slot="{ item: notification }"
|
||||
:items="notifications"
|
||||
class="elsfgstc"
|
||||
:no-gap="true"
|
||||
>
|
||||
|
@ -71,7 +71,7 @@ import { foldNotifications } from "@/scripts/fold";
|
|||
import { defaultStore } from "@/store";
|
||||
|
||||
const props = defineProps<{
|
||||
includeTypes?: (typeof notificationTypes)[number][];
|
||||
includeTypes?: (typeof notificationTypes)[number][] | null;
|
||||
unreadOnly?: boolean;
|
||||
}>();
|
||||
|
||||
|
|
|
@ -43,6 +43,7 @@ import { i18n } from "@/i18n";
|
|||
import type { PageMetadata } from "@/scripts/page-metadata";
|
||||
import { provideMetadataReceiver } from "@/scripts/page-metadata";
|
||||
import icon from "@/scripts/icon";
|
||||
import * as os from "@/os";
|
||||
|
||||
const props = defineProps<{
|
||||
initialPath: string;
|
||||
|
@ -121,6 +122,7 @@ const contextmenu = computed(() => [
|
|||
text: i18n.ts.copyLink,
|
||||
action: () => {
|
||||
copyToClipboard(url + router.getCurrentPath());
|
||||
os.success();
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
>
|
||||
<MkLoading v-if="fetching" />
|
||||
|
||||
<MkError v-else-if="error" @retry="init()" />
|
||||
<MkError v-else-if="error" @retry="reload()" />
|
||||
|
||||
<div v-else-if="empty" key="_empty_" class="empty">
|
||||
<slot name="empty">
|
||||
|
@ -38,7 +38,7 @@
|
|||
</MkButton>
|
||||
<MkLoading v-else class="loading" />
|
||||
</div>
|
||||
<slot :items="items" :foldedItems="foldedItems"></slot>
|
||||
<slot :items="items" :folded-items="foldedItems"></slot>
|
||||
<div
|
||||
v-show="!pagination.reversed && more"
|
||||
key="_more_"
|
||||
|
@ -105,9 +105,9 @@ export type MkPaginationType<
|
|||
updateItem: (id: string, replacer: (old: Item) => Item) => boolean;
|
||||
};
|
||||
|
||||
export type PagingAble = {
|
||||
export interface PagingAble {
|
||||
id: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type PagingKeyOf<T> = TypeUtils.EndpointsOf<T[]>;
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Used Intentionally
|
||||
|
@ -173,11 +173,15 @@ const rootEl = ref<HTMLElement>();
|
|||
const items = ref<Item[]>([]);
|
||||
const foldedItems = ref([]) as Ref<Fold[]>;
|
||||
|
||||
function toReversed<T>(arr: T[]) {
|
||||
return [...arr].reverse();
|
||||
}
|
||||
|
||||
// To improve performance, we do not use vue’s `computed` here
|
||||
function calculateItems() {
|
||||
function getItems<T>(folder: (ns: Item[]) => T[]) {
|
||||
const res = [
|
||||
folder(prepended.value.toReversed()),
|
||||
folder(toReversed(prepended.value)),
|
||||
...arrItems.value.map((arr) => folder(arr)),
|
||||
folder(appended.value),
|
||||
].flat(1);
|
||||
|
@ -242,6 +246,8 @@ const reload = (): Promise<void> => {
|
|||
appended.value = [];
|
||||
prepended.value = [];
|
||||
idMap.clear();
|
||||
offset.value = 0;
|
||||
nextPagingBy = {};
|
||||
return init();
|
||||
};
|
||||
|
||||
|
@ -349,7 +355,7 @@ async function fetch(firstFetching?: boolean) {
|
|||
|
||||
if (firstFetching && props.folder != null) {
|
||||
// In this way, prepended has some initial values for folding
|
||||
prepended.value = res.toReversed();
|
||||
prepended.value = toReversed(res);
|
||||
} else {
|
||||
// For ascending and offset modes, append and prepend may cause item duplication
|
||||
// so they need to be filtered out.
|
||||
|
@ -396,7 +402,7 @@ const prepend = (...item: Item[]): void => {
|
|||
prepended.value.length >
|
||||
(props.pagination.secondFetchLimit || SECOND_FETCH_LIMIT_DEFAULT)
|
||||
) {
|
||||
arrItems.value.unshift(prepended.value.toReversed());
|
||||
arrItems.value.unshift(toReversed(prepended.value));
|
||||
prepended.value = [];
|
||||
// We don't need to calculate here because it won't cause any changes in items
|
||||
}
|
||||
|
@ -477,7 +483,7 @@ const updateItem = (id: Item["id"], replacer: (old: Item) => Item): boolean => {
|
|||
};
|
||||
|
||||
if (props.pagination.params && isRef<Param>(props.pagination.params)) {
|
||||
watch(props.pagination.params, init, { deep: true });
|
||||
watch(props.pagination.params, reload, { deep: true });
|
||||
}
|
||||
|
||||
watch(
|
||||
|
|
|
@ -373,6 +373,11 @@ const props = withDefaults(
|
|||
autofocus?: boolean;
|
||||
showMfmCheatSheet?: boolean;
|
||||
editId?: entities.Note["id"];
|
||||
selectRange?: [
|
||||
start: number,
|
||||
end: number,
|
||||
direction?: "forward" | "backward" | "none",
|
||||
];
|
||||
}>(),
|
||||
{
|
||||
initialVisibleUsers: () => [],
|
||||
|
@ -692,10 +697,14 @@ function togglePoll() {
|
|||
function focus() {
|
||||
if (textareaEl.value) {
|
||||
textareaEl.value.focus();
|
||||
textareaEl.value.setSelectionRange(
|
||||
textareaEl.value.value.length,
|
||||
textareaEl.value.value.length,
|
||||
);
|
||||
if (props.selectRange) {
|
||||
textareaEl.value.setSelectionRange(...props.selectRange);
|
||||
} else {
|
||||
textareaEl.value.setSelectionRange(
|
||||
textareaEl.value.value.length,
|
||||
textareaEl.value.value.length,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -44,6 +44,11 @@ const props = defineProps<{
|
|||
fixed?: boolean;
|
||||
autofocus?: boolean;
|
||||
editId?: entities.Note["id"];
|
||||
selectRange?: [
|
||||
start: number,
|
||||
end: number,
|
||||
direction?: "forward" | "backward" | "none",
|
||||
];
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
|
|
|
@ -44,6 +44,7 @@ const FIRE_THRESHOLD = defaultStore.state.pullToRefreshThreshold;
|
|||
const RELEASE_TRANSITION_DURATION = 200;
|
||||
const PULL_BRAKE_BASE = 1.5;
|
||||
const PULL_BRAKE_FACTOR = 170;
|
||||
const MAX_PULL_TAN_ANGLE = Math.tan((1 / 6) * Math.PI); // 30°
|
||||
|
||||
const pullStarted = ref(false);
|
||||
const pullEnded = ref(false);
|
||||
|
@ -53,6 +54,7 @@ const pullDistance = ref(0);
|
|||
let disabled = false;
|
||||
const supportPointerDesktop = false;
|
||||
let startScreenY: number | null = null;
|
||||
let startScreenX: number | null = null;
|
||||
|
||||
const rootEl = shallowRef<HTMLDivElement>();
|
||||
let scrollEl: HTMLElement | null = null;
|
||||
|
@ -72,11 +74,16 @@ function getScreenY(event) {
|
|||
if (supportPointerDesktop) return event.screenY;
|
||||
return event.touches[0].screenY;
|
||||
}
|
||||
function getScreenX(event) {
|
||||
if (supportPointerDesktop) return event.screenX;
|
||||
return event.touches[0].screenX;
|
||||
}
|
||||
|
||||
function moveStart(event) {
|
||||
if (!pullStarted.value && !isRefreshing.value && !disabled) {
|
||||
pullStarted.value = true;
|
||||
startScreenY = getScreenY(event);
|
||||
startScreenX = getScreenX(event);
|
||||
pullDistance.value = 0;
|
||||
}
|
||||
}
|
||||
|
@ -117,6 +124,7 @@ async function closeContent() {
|
|||
function moveEnd() {
|
||||
if (pullStarted.value && !isRefreshing.value) {
|
||||
startScreenY = null;
|
||||
startScreenX = null;
|
||||
if (pullEnded.value) {
|
||||
pullEnded.value = false;
|
||||
isRefreshing.value = true;
|
||||
|
@ -146,11 +154,17 @@ function moving(event: TouchEvent | PointerEvent) {
|
|||
moveEnd();
|
||||
return;
|
||||
}
|
||||
if (startScreenY === null) {
|
||||
startScreenY = getScreenY(event);
|
||||
}
|
||||
startScreenX ??= getScreenX(event);
|
||||
startScreenY ??= getScreenY(event);
|
||||
const moveScreenY = getScreenY(event);
|
||||
const moveScreenX = getScreenX(event);
|
||||
const moveHeight = moveScreenY - startScreenY!;
|
||||
const moveWidth = moveScreenX - startScreenX!;
|
||||
if (Math.abs(moveWidth / moveHeight) > MAX_PULL_TAN_ANGLE) {
|
||||
if (Math.abs(moveWidth) > 30) pullStarted.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
pullDistance.value = Math.min(Math.max(moveHeight, 0), MAX_PULL_DISTANCE);
|
||||
|
||||
if (pullDistance.value > 0) {
|
||||
|
@ -203,7 +217,7 @@ function unregisterEventListenersForReadyToPull() {
|
|||
onMounted(() => {
|
||||
if (rootEl.value == null) return;
|
||||
|
||||
scrollEl = getScrollContainer(rootEl.value);
|
||||
scrollEl = getScrollContainer(rootEl.value) ?? document.querySelector("HTML");
|
||||
if (scrollEl == null) return;
|
||||
|
||||
scrollEl.addEventListener("scroll", onScrollContainerScroll, {
|
||||
|
|
|
@ -15,12 +15,12 @@
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { shallowRef } from "vue";
|
||||
import QRCodeVue3 from "qrcode-vue3";
|
||||
import MkModal from "@/components/MkModal.vue";
|
||||
import MkButton from "@/components/MkButton.vue";
|
||||
import { i18n } from "@/i18n";
|
||||
import QRCodeVue3 from "qrcode-vue3";
|
||||
|
||||
const props = defineProps<{
|
||||
defineProps<{
|
||||
qrCode: string;
|
||||
}>();
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<template>
|
||||
<button
|
||||
v-if="canRenote && defaultStore.state.seperateRenoteQuote"
|
||||
ref="el"
|
||||
v-tooltip.noDelay.bottom="i18n.ts.quote"
|
||||
class="eddddedb _button"
|
||||
@click.stop="quote()"
|
||||
|
@ -10,8 +11,8 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from "vue";
|
||||
import type { entities } from "firefish-js";
|
||||
import { computed, ref } from "vue";
|
||||
import { acct, type entities } from "firefish-js";
|
||||
import { pleaseLogin } from "@/scripts/please-login";
|
||||
import * as os from "@/os";
|
||||
import { me } from "@/me";
|
||||
|
@ -23,6 +24,8 @@ const props = defineProps<{
|
|||
note: entities.Note;
|
||||
}>();
|
||||
|
||||
const el = ref<HTMLButtonElement>();
|
||||
|
||||
const canRenote = computed(
|
||||
() =>
|
||||
["public", "home"].includes(props.note.visibility) ||
|
||||
|
@ -31,10 +34,57 @@ const canRenote = computed(
|
|||
|
||||
function quote(): void {
|
||||
pleaseLogin();
|
||||
if (
|
||||
props.note.renote != null &&
|
||||
(props.note.text != null ||
|
||||
props.note.fileIds.length === 0 ||
|
||||
props.note.poll != null)
|
||||
) {
|
||||
menu();
|
||||
} else {
|
||||
normalQuote();
|
||||
}
|
||||
}
|
||||
|
||||
function normalQuote(): void {
|
||||
os.post({
|
||||
renote: props.note,
|
||||
});
|
||||
}
|
||||
|
||||
function slashQuote(): void {
|
||||
os.post({
|
||||
initialText: ` // @${acct.toString(props.note.user)}: ${props.note.text}`,
|
||||
selectRange: [0, 0],
|
||||
renote: props.note.renote,
|
||||
channel: props.note.channel,
|
||||
});
|
||||
}
|
||||
|
||||
function menu(viaKeyboard = false): void {
|
||||
os.popupMenu(
|
||||
[
|
||||
{
|
||||
text: i18n.ts.quote,
|
||||
icon: `${icon("ph-quotes")}`,
|
||||
action: normalQuote,
|
||||
},
|
||||
{
|
||||
text: i18n.ts.slashQuote,
|
||||
icon: `${icon("ph-notches")}`,
|
||||
action: slashQuote,
|
||||
},
|
||||
],
|
||||
el.value,
|
||||
{
|
||||
viaKeyboard,
|
||||
},
|
||||
).then(focus);
|
||||
}
|
||||
|
||||
function focus(): void {
|
||||
el.value!.focus();
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
|
@ -25,8 +25,8 @@
|
|||
</div>
|
||||
<MkPagination
|
||||
ref="pagingComponent"
|
||||
:pagination="pagination"
|
||||
v-slot="{ items }"
|
||||
:pagination="pagination"
|
||||
>
|
||||
<MkUserCardMini v-for="{ user: user } in items" :key="user.id" :user="user" />
|
||||
</MkPagination>
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from "vue";
|
||||
import type { entities } from "firefish-js";
|
||||
import { acct, type entities } from "firefish-js";
|
||||
import Ripple from "@/components/MkRipple.vue";
|
||||
import XDetails from "@/components/MkUsersTooltip.vue";
|
||||
import { pleaseLogin } from "@/scripts/please-login";
|
||||
|
@ -223,6 +223,28 @@ const renote = (viaKeyboard = false, ev?: MouseEvent) => {
|
|||
});
|
||||
},
|
||||
});
|
||||
if (
|
||||
props.note.renote != null &&
|
||||
(props.note.text != null ||
|
||||
props.note.fileIds.length === 0 ||
|
||||
props.note.poll != null)
|
||||
) {
|
||||
buttonActions.push({
|
||||
text: i18n.ts.slashQuote,
|
||||
icon: `${icon("ph-notches")}`,
|
||||
danger: false,
|
||||
action: () => {
|
||||
os.post({
|
||||
initialText: ` // @${acct.toString(props.note.user)}: ${
|
||||
props.note.text
|
||||
}`,
|
||||
selectRange: [0, 0],
|
||||
renote: props.note.renote,
|
||||
channel: props.note.channel,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (hasRenotedBefore.value) {
|
||||
|
|
|
@ -382,7 +382,7 @@ function focusFooter(ev) {
|
|||
> .body {
|
||||
min-height: 2em;
|
||||
max-height: 5em;
|
||||
filter: blur(4px);
|
||||
filter: blur(5px);
|
||||
:deep(span) {
|
||||
animation: none !important;
|
||||
transform: none !important;
|
||||
|
|
|
@ -80,6 +80,7 @@ function onContextmenu(ev) {
|
|||
text: i18n.ts.copyLink,
|
||||
action: () => {
|
||||
copyToClipboard(`${url}${props.to}`);
|
||||
os.success();
|
||||
},
|
||||
},
|
||||
],
|
||||
|
|
|
@ -27,6 +27,7 @@
|
|||
>
|
||||
<swiper-slide>
|
||||
<XNotifications
|
||||
:key="'tab1'"
|
||||
class="notifications"
|
||||
:include-types="includeTypes"
|
||||
:unread-only="false"
|
||||
|
@ -34,16 +35,18 @@
|
|||
</swiper-slide>
|
||||
<swiper-slide>
|
||||
<XNotifications
|
||||
v-if="tab === 'reactions'"
|
||||
:key="'tab2'"
|
||||
class="notifications"
|
||||
:include-types="['reaction']"
|
||||
:unread-only="false"
|
||||
/>
|
||||
</swiper-slide>
|
||||
<swiper-slide>
|
||||
<XNotes :pagination="mentionsPagination" />
|
||||
<XNotes v-if="tab === 'mentions'" :key="'tab3'" :pagination="mentionsPagination" />
|
||||
</swiper-slide>
|
||||
<swiper-slide>
|
||||
<XNotes :pagination="directNotesPagination" />
|
||||
<XNotes v-if="tab === 'directNotes'" :key="'tab4'" :pagination="directNotesPagination" />
|
||||
</swiper-slide>
|
||||
</swiper>
|
||||
</MkSpacer>
|
||||
|
@ -54,6 +57,7 @@
|
|||
import { computed, ref, watch } from "vue";
|
||||
import { Virtual } from "swiper/modules";
|
||||
import { Swiper, SwiperSlide } from "swiper/vue";
|
||||
import type { Swiper as SwiperType } from "swiper/types";
|
||||
import { notificationTypes } from "firefish-js";
|
||||
import XNotifications from "@/components/MkNotifications.vue";
|
||||
import XNotes from "@/components/MkNotes.vue";
|
||||
|
@ -70,7 +74,7 @@ const tabs = ["all", "reactions", "mentions", "directNotes"];
|
|||
const tab = ref(tabs[0]);
|
||||
watch(tab, () => syncSlide(tabs.indexOf(tab.value)));
|
||||
|
||||
const includeTypes = ref<string[] | null>(null);
|
||||
const includeTypes = ref<(typeof notificationTypes)[number][] | null>(null);
|
||||
os.api("notifications/mark-all-as-read");
|
||||
|
||||
const MOBILE_THRESHOLD = 500;
|
||||
|
@ -98,7 +102,7 @@ const directNotesPagination = {
|
|||
function setFilter(ev) {
|
||||
const typeItems = notificationTypes.map((t) => ({
|
||||
text: i18n.t(`_notification._types.${t}`),
|
||||
active: includeTypes.value && includeTypes.value.includes(t),
|
||||
active: includeTypes.value?.includes(t),
|
||||
action: () => {
|
||||
includeTypes.value = [t];
|
||||
},
|
||||
|
@ -121,25 +125,23 @@ function setFilter(ev) {
|
|||
}
|
||||
|
||||
const headerActions = computed(() =>
|
||||
[
|
||||
tab.value === "all"
|
||||
? {
|
||||
tab.value === "all"
|
||||
? [
|
||||
{
|
||||
text: i18n.ts.filter,
|
||||
icon: `${icon("ph-funnel")}`,
|
||||
highlighted: includeTypes.value != null,
|
||||
handler: setFilter,
|
||||
}
|
||||
: undefined,
|
||||
tab.value === "all"
|
||||
? {
|
||||
},
|
||||
{
|
||||
text: i18n.ts.markAllAsRead,
|
||||
icon: `${icon("ph-check")}`,
|
||||
handler: () => {
|
||||
os.apiWithDialog("notifications/mark-all-as-read");
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
].filter((x) => x !== undefined),
|
||||
},
|
||||
]
|
||||
: [],
|
||||
);
|
||||
|
||||
const headerTabs = computed(() => [
|
||||
|
@ -172,18 +174,19 @@ definePageMetadata(
|
|||
})),
|
||||
);
|
||||
|
||||
let swiperRef = null;
|
||||
let swiperRef: SwiperType | null = null;
|
||||
|
||||
function setSwiperRef(swiper) {
|
||||
function setSwiperRef(swiper: SwiperType) {
|
||||
swiperRef = swiper;
|
||||
syncSlide(tabs.indexOf(tab.value));
|
||||
}
|
||||
|
||||
function onSlideChange() {
|
||||
tab.value = tabs[swiperRef.activeIndex];
|
||||
if (tab.value !== tabs[swiperRef!.activeIndex])
|
||||
tab.value = tabs[swiperRef!.activeIndex];
|
||||
}
|
||||
|
||||
function syncSlide(index) {
|
||||
swiperRef.slideTo(index);
|
||||
function syncSlide(index: number) {
|
||||
if (index !== swiperRef!.activeIndex) swiperRef!.slideTo(index);
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -131,6 +131,10 @@
|
|||
i18n.ts.autocorrectNoteLanguage
|
||||
}}</FormSwitch>
|
||||
|
||||
<FormSwitch v-model="foldNotification" class="_formBlock">{{
|
||||
i18n.ts.foldNotification
|
||||
}}</FormSwitch>
|
||||
|
||||
<FormSelect v-model="serverDisconnectedBehavior" class="_formBlock">
|
||||
<template #label>{{ i18n.ts.whenServerDisconnected }}</template>
|
||||
<option value="reload">
|
||||
|
@ -327,13 +331,6 @@
|
|||
</FormSelect>
|
||||
</FormSection>
|
||||
|
||||
<FormSection>
|
||||
<template #label>{{ i18n.ts.experimentalFeatures }}</template>
|
||||
<FormSwitch v-model="foldNotification" class="_formBlock">{{
|
||||
i18n.ts.foldNotification
|
||||
}}</FormSwitch>
|
||||
</FormSection>
|
||||
|
||||
<FormSection>
|
||||
<template #label>{{ i18n.ts.forMobile }}</template>
|
||||
<FormSwitch
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import type { entities } from "firefish-js";
|
||||
import type {
|
||||
FoldableNotification,
|
||||
NotificationFolded,
|
||||
} from "@/types/notification";
|
||||
import type { entities } from "firefish-js";
|
||||
|
||||
interface FoldOption {
|
||||
/** If items length is 1, skip aggregation */
|
||||
|
|
|
@ -246,6 +246,7 @@ export function getUserMenu(user, router: Router = mainRouter) {
|
|||
text: i18n.ts.copyUsername,
|
||||
action: () => {
|
||||
copyToClipboard(`@${user.username}@${user.host || host}`);
|
||||
os.success();
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -272,6 +273,7 @@ export function getUserMenu(user, router: Router = mainRouter) {
|
|||
text: i18n.ts.copyRemoteFollowUrl,
|
||||
action: () => {
|
||||
copyToClipboard(`https://${host}/follow-me?acct=${user.username}`);
|
||||
os.success();
|
||||
},
|
||||
},
|
||||
],
|
||||
|
@ -286,6 +288,7 @@ export function getUserMenu(user, router: Router = mainRouter) {
|
|||
text: i18n.ts._feeds.rss,
|
||||
action: () => {
|
||||
copyToClipboard(`https://${host}/@${user.username}.rss`);
|
||||
os.success();
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -293,6 +296,7 @@ export function getUserMenu(user, router: Router = mainRouter) {
|
|||
text: i18n.ts._feeds.atom,
|
||||
action: () => {
|
||||
copyToClipboard(`https://${host}/@${user.username}.atom`);
|
||||
os.success();
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -300,6 +304,7 @@ export function getUserMenu(user, router: Router = mainRouter) {
|
|||
text: i18n.ts._feeds.jsonFeed,
|
||||
action: () => {
|
||||
copyToClipboard(`https://${host}/@${user.username}.json`);
|
||||
os.success();
|
||||
},
|
||||
},
|
||||
],
|
||||
|
|
|
@ -452,7 +452,7 @@ export const defaultStore = markRaw(
|
|||
},
|
||||
foldNotification: {
|
||||
where: "deviceAccount",
|
||||
default: false,
|
||||
default: true,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
|
|
@ -4,7 +4,7 @@ export type FoldableNotification =
|
|||
| entities.RenoteNotification
|
||||
| entities.ReactionNotification;
|
||||
|
||||
type Fold<T extends FoldableNotification> = {
|
||||
interface Fold<T extends FoldableNotification> {
|
||||
id: string;
|
||||
type: T["type"];
|
||||
createdAt: T["createdAt"];
|
||||
|
@ -13,7 +13,7 @@ type Fold<T extends FoldableNotification> = {
|
|||
userIds: entities.User["id"][];
|
||||
users: entities.User[];
|
||||
notifications: T[];
|
||||
};
|
||||
}
|
||||
|
||||
export type RenoteNotificationFolded = Fold<entities.RenoteNotification>;
|
||||
|
||||
|
|
Loading…
Reference in a new issue