Compare commits

...

6 commits

Author SHA1 Message Date
Amelia Yukii
78975ddcc8 merge: Switch source code bits (!435)
View MR for information: https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/435

Approved-by: Julia Johannesen <julia@insertdomain.name>
Approved-by: Amelia Yukii <amelia.yukii@shourai.de>
2024-02-20 20:01:31 +00:00
dakkar
c9940b7359 Switch source code bits 2024-02-20 20:01:31 +00:00
Amelia Yukii
ff8cfeb05c merge: avoid look-behind in nyaize - fixes 419 (!433)
View MR for information: https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/433

Approved-by: Julia Johannesen <julia@insertdomain.name>
Approved-by: Amelia Yukii <amelia.yukii@shourai.de>
2024-02-20 20:00:22 +00:00
dakkar
b02ded29dd avoid look-behind in nyaize - fixes 419 2024-02-20 20:00:22 +00:00
Amelia Yukii
27ea1b7133 merge: Note Edited notification type (!432)
View MR for information: https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/432

Closes #403

Approved-by: Amelia Yukii <amelia.yukii@shourai.de>
Approved-by: dakkar <dakkar@thenautilus.net>
2024-02-20 15:10:41 +00:00
Marie
4a13508da0 Note Edited notification type 2024-02-20 15:10:41 +00:00
23 changed files with 202 additions and 104 deletions

View file

@ -1026,6 +1026,7 @@ remindMeLater: "Maybe later"
didYouLikeMisskey: "Have you taken a liking to Sharkey?" didYouLikeMisskey: "Have you taken a liking to Sharkey?"
pleaseDonate: "{host} uses the free software, Sharkey. We would highly appreciate your donations so development of Sharkey can continue!" pleaseDonate: "{host} uses the free software, Sharkey. We would highly appreciate your donations so development of Sharkey can continue!"
pleaseDonateInstance: "You can also support {host} directly by donating to your instance administration." pleaseDonateInstance: "You can also support {host} directly by donating to your instance administration."
correspondingSourceIsAvailable: "The corresponding source code is available from {anchor}."
roles: "Roles" roles: "Roles"
role: "Role" role: "Role"
noRole: "Role not found" noRole: "Role not found"
@ -1214,6 +1215,12 @@ confirmShowRepliesAll: "This operation is irreversible. Would you really like to
confirmHideRepliesAll: "This operation is irreversible. Would you really like to hide replies to others from everyone you follow in your timeline?" confirmHideRepliesAll: "This operation is irreversible. Would you really like to hide replies to others from everyone you follow in your timeline?"
externalServices: "External Services" externalServices: "External Services"
sourceCode: "Source code" sourceCode: "Source code"
sourceCodeIsNotYetProvided: "The source code is not yet available. Please contact your administrator to fix this issue."
repositoryUrl: "Repository URL"
repositoryUrlDescription: "If there is a repository where the source code is publicly available, enter its URL. If you are using Sharkey as-is (without any changes to the source code), enter https://activitypub.software/TransFem-org/Sharkey/."
repositoryUrlOrTarballRequired: "If you don't have a public repository, you'll need to provide a tarball instead. See .config/example.yml for details."
feedback: "Feedback"
feedbackUrl: "Feedback URL"
impressum: "Impressum" impressum: "Impressum"
impressumUrl: "Impressum URL" impressumUrl: "Impressum URL"
impressumDescription: "In some countries, like germany, the inclusion of operator contact information (an Impressum) is legally required for commercial websites." impressumDescription: "In some countries, like germany, the inclusion of operator contact information (an Impressum) is legally required for commercial websites."
@ -1808,8 +1815,12 @@ _aboutMisskey:
contributors: "Main contributors" contributors: "Main contributors"
allContributors: "All contributors" allContributors: "All contributors"
source: "Source code" source: "Source code"
original: "Misskey original"
original_sharkey: "Sharkey original"
thisIsModifiedVersion: "{name} uses a modified version of the original Sharkey"
translation: "Translate Sharkey" translation: "Translate Sharkey"
donate: "Donate to Sharkey" donate: "Donate to Misskey"
donate_sharkey: "Donate to Sharkey"
morePatrons: "We also appreciate the support of many other helpers not listed here. Thank you! 🥰" morePatrons: "We also appreciate the support of many other helpers not listed here. Thank you! 🥰"
patrons: "Patrons" patrons: "Patrons"
projectMembers: "Project members" projectMembers: "Project members"
@ -2334,6 +2345,7 @@ _notification:
reactedBySomeUsers: "{n} users reacted" reactedBySomeUsers: "{n} users reacted"
renotedBySomeUsers: "Boosted by {n} users" renotedBySomeUsers: "Boosted by {n} users"
followedBySomeUsers: "Followed by {n} users" followedBySomeUsers: "Followed by {n} users"
edited: "Note got edited"
_types: _types:
all: "All" all: "All"
note: "New notes" note: "New notes"

18
locales/index.d.ts vendored
View file

@ -7059,11 +7059,15 @@ export interface Locale extends ILocale {
*/ */
"source": string; "source": string;
/** /**
* * Misskey
*/ */
"original": string; "original": string;
/** /**
* {name}Misskeyを改変したバージョンを使用しています * Sharkey
*/
"original_sharkey": string;
/**
* {name}Sharkeyを改変したバージョンを使用しています
*/ */
"thisIsModifiedVersion": ParameterizedString<"name">; "thisIsModifiedVersion": ParameterizedString<"name">;
/** /**
@ -7071,9 +7075,13 @@ export interface Locale extends ILocale {
*/ */
"translation": string; "translation": string;
/** /**
* Sharkeyに寄付 * Misskeyに寄付
*/ */
"donate": string; "donate": string;
/**
* Sharkeyに寄付
*/
"donate_sharkey": string;
/** /**
* 🥰 * 🥰
*/ */
@ -9048,6 +9056,10 @@ export interface Locale extends ILocale {
* *
*/ */
"pollEnded": string; "pollEnded": string;
/**
* 稿
*/
"edited": string;
/** /**
* 稿 * 稿
*/ */

View file

@ -1839,10 +1839,12 @@ _aboutMisskey:
contributors: "主なコントリビューター" contributors: "主なコントリビューター"
allContributors: "全てのコントリビューター" allContributors: "全てのコントリビューター"
source: "ソースコード" source: "ソースコード"
original: "オリジナル" original: "Misskey オリジナル"
thisIsModifiedVersion: "{name}はオリジナルのMisskeyを改変したバージョンを使用しています。" original_sharkey: "Sharkey オリジナル"
thisIsModifiedVersion: "{name}はオリジナルのSharkeyを改変したバージョンを使用しています。"
translation: "Sharkeyを翻訳" translation: "Sharkeyを翻訳"
donate: "Sharkeyに寄付" donate: "Misskeyに寄付"
donate_sharkey: "Sharkeyに寄付"
morePatrons: "他にも多くの方が支援してくれています。ありがとうございます🥰" morePatrons: "他にも多くの方が支援してくれています。ありがとうございます🥰"
patrons: "支援者" patrons: "支援者"
projectMembers: "プロジェクトメンバー" projectMembers: "プロジェクトメンバー"
@ -2389,6 +2391,7 @@ _notification:
youReceivedFollowRequest: "フォローリクエストが来ました" youReceivedFollowRequest: "フォローリクエストが来ました"
yourFollowRequestAccepted: "フォローリクエストが承認されました" yourFollowRequestAccepted: "フォローリクエストが承認されました"
pollEnded: "アンケートの結果が出ました" pollEnded: "アンケートの結果が出ました"
edited: "投稿が編集されました"
newNote: "新しい投稿" newNote: "新しい投稿"
unreadAntennaNote: "アンテナ {name}" unreadAntennaNote: "アンテナ {name}"
roleAssigned: "ロールが付与されました" roleAssigned: "ロールが付与されました"

View file

@ -96,6 +96,7 @@ export interface MainEventTypes {
announcementCreated: { announcementCreated: {
announcement: Packed<'Announcement'>; announcement: Packed<'Announcement'>;
}; };
edited: Packed<'Note'>;
} }
export interface DriveEventTypes { export interface DriveEventTypes {

View file

@ -52,7 +52,7 @@ import { isReply } from '@/misc/is-reply.js';
import { trackPromise } from '@/misc/promise-tracker.js'; import { trackPromise } from '@/misc/promise-tracker.js';
import { isUserRelated } from '@/misc/is-user-related.js'; import { isUserRelated } from '@/misc/is-user-related.js';
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention'; type NotificationType = 'reply' | 'renote' | 'quote' | 'mention' | 'edited';
class NotificationManager { class NotificationManager {
private notifier: { id: MiUser['id']; }; private notifier: { id: MiUser['id']; };
@ -586,7 +586,7 @@ export class NoteEditService implements OnApplicationShutdown {
} }
// Pack the note // Pack the note
const noteObj = await this.noteEntityService.pack(note, null, { skipHide: true }); const noteObj = await this.noteEntityService.pack(note, null, { skipHide: true, withReactionAndUserPairCache: true });
if (data.poll != null) { if (data.poll != null) {
this.globalEventService.publishNoteStream(note.id, 'updated', { this.globalEventService.publishNoteStream(note.id, 'updated', {
cw: note.cw, cw: note.cw,
@ -612,7 +612,7 @@ export class NoteEditService implements OnApplicationShutdown {
const nm = new NotificationManager(this.mutingsRepository, this.notificationService, user, note); const nm = new NotificationManager(this.mutingsRepository, this.notificationService, user, note);
await this.createMentionedEvents(mentionedUsers, note, nm); //await this.createMentionedEvents(mentionedUsers, note, nm);
// If has in reply to note // If has in reply to note
if (data.reply) { if (data.reply) {
@ -634,12 +634,12 @@ export class NoteEditService implements OnApplicationShutdown {
const muted = isUserRelated(note, userIdsWhoMeMuting); const muted = isUserRelated(note, userIdsWhoMeMuting);
if (!isThreadMuted && !muted) { if (!isThreadMuted && !muted) {
nm.push(data.reply.userId, 'reply'); nm.push(data.reply.userId, 'edited');
this.globalEventService.publishMainStream(data.reply.userId, 'reply', noteObj); this.globalEventService.publishMainStream(data.reply.userId, 'edited', noteObj);
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === data.reply!.userId && x.on.includes('reply')); const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === data.reply!.userId && x.on.includes('edited'));
for (const webhook of webhooks) { for (const webhook of webhooks) {
this.queueService.webhookDeliver(webhook, 'reply', { this.queueService.webhookDeliver(webhook, 'edited', {
note: noteObj, note: noteObj,
}); });
} }
@ -647,45 +647,6 @@ export class NoteEditService implements OnApplicationShutdown {
} }
} }
// If it is renote
if (data.renote) {
const type = this.isQuote(data) ? 'quote' : 'renote';
// Notify
if (data.renote.userHost === null) {
const isThreadMuted = await this.noteThreadMutingsRepository.exists({
where: {
userId: data.renote.userId,
threadId: data.renote.threadId ?? data.renote.id,
},
});
const [
userIdsWhoMeMuting,
] = data.renote.userId ? await Promise.all([
this.cacheService.userMutingsCache.fetch(data.renote.userId),
]) : [new Set<string>()];
const muted = isUserRelated(note, userIdsWhoMeMuting);
if (!isThreadMuted && !muted) {
nm.push(data.renote.userId, type);
}
}
// Publish event
if ((user.id !== data.renote.userId) && data.renote.userHost === null) {
this.globalEventService.publishMainStream(data.renote.userId, 'renote', noteObj);
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === data.renote!.userId && x.on.includes('renote'));
for (const webhook of webhooks) {
this.queueService.webhookDeliver(webhook, 'renote', {
note: noteObj,
});
}
}
}
nm.notify(); nm.notify();
//#region AP deliver //#region AP deliver
@ -780,17 +741,17 @@ export class NoteEditService implements OnApplicationShutdown {
detail: true, detail: true,
}); });
this.globalEventService.publishMainStream(u.id, 'mention', detailPackedNote); this.globalEventService.publishMainStream(u.id, 'edited', detailPackedNote);
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === u.id && x.on.includes('mention')); const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === u.id && x.on.includes('edited'));
for (const webhook of webhooks) { for (const webhook of webhooks) {
this.queueService.webhookDeliver(webhook, 'mention', { this.queueService.webhookDeliver(webhook, 'edited', {
note: detailPackedNote, note: detailPackedNote,
}); });
} }
// Create notification // Create notification
nm.push(u.id, 'mention'); nm.push(u.id, 'edited');
} }
} }

View file

@ -20,8 +20,8 @@ import type { OnModuleInit } from '@nestjs/common';
import type { UserEntityService } from './UserEntityService.js'; import type { UserEntityService } from './UserEntityService.js';
import type { NoteEntityService } from './NoteEntityService.js'; import type { NoteEntityService } from './NoteEntityService.js';
const NOTE_REQUIRED_NOTIFICATION_TYPES = new Set(['note', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollEnded'] as (typeof notificationTypes[number])[]); const NOTE_REQUIRED_NOTIFICATION_TYPES = new Set(['note', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollEnded', 'edited'] as (typeof notificationTypes[number])[]);
const NOTE_REQUIRED_GROUPED_NOTIFICATION_TYPES = new Set(['note', 'mention', 'reply', 'renote', 'renote:grouped', 'quote', 'reaction', 'reaction:grouped', 'pollEnded']); const NOTE_REQUIRED_GROUPED_NOTIFICATION_TYPES = new Set(['note', 'mention', 'reply', 'renote', 'renote:grouped', 'quote', 'reaction', 'reaction:grouped', 'pollEnded', 'edited']);
@Injectable() @Injectable()
export class NotificationEntityService implements OnModuleInit { export class NotificationEntityService implements OnModuleInit {

View file

@ -107,6 +107,12 @@ export type MiNotification = {
type: 'test'; type: 'test';
id: string; id: string;
createdAt: string; createdAt: string;
} | {
type: 'edited';
id: string;
createdAt: string;
notifierId: MiUser['id'];
noteId: MiNote['id'];
}; };
export type MiGroupedNotification = MiNotification | { export type MiGroupedNotification = MiNotification | {

View file

@ -7,7 +7,7 @@ import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typ
import { id } from './util/id.js'; import { id } from './util/id.js';
import { MiUser } from './User.js'; import { MiUser } from './User.js';
export const webhookEventTypes = ['mention', 'unfollow', 'follow', 'followed', 'note', 'reply', 'renote', 'reaction'] as const; export const webhookEventTypes = ['mention', 'unfollow', 'follow', 'followed', 'note', 'reply', 'renote', 'reaction', 'edited'] as const;
@Entity('webhook') @Entity('webhook')
export class MiWebhook { export class MiWebhook {

View file

@ -318,6 +318,31 @@ export const packedNotificationSchema = {
optional: false, nullable: false, optional: false, nullable: false,
}, },
}, },
}, {
type: 'object',
properties: {
...baseSchema.properties,
type: {
type: 'string',
optional: false, nullable: false,
enum: ['edited'],
},
user: {
type: 'object',
ref: 'UserLite',
optional: false, nullable: false,
},
userId: {
type: 'string',
optional: false, nullable: false,
format: 'id',
},
note: {
type: 'object',
ref: 'Note',
optional: false, nullable: false,
},
},
}, { }, {
type: 'object', type: 'object',
properties: { properties: {

View file

@ -164,7 +164,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
groupedNotifications = groupedNotifications.slice(0, ps.limit); groupedNotifications = groupedNotifications.slice(0, ps.limit);
const noteIds = groupedNotifications const noteIds = groupedNotifications
.filter((notification): notification is FilterUnionByProperty<MiNotification, 'type', 'mention' | 'reply' | 'quote'> => ['mention', 'reply', 'quote'].includes(notification.type)) .filter((notification): notification is FilterUnionByProperty<MiNotification, 'type', 'mention' | 'reply' | 'quote' | 'edited'> => ['mention', 'reply', 'quote', 'edited'].includes(notification.type))
.map(notification => notification.noteId!); .map(notification => notification.noteId!);
if (noteIds.length > 0) { if (noteIds.length > 0) {

View file

@ -113,7 +113,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
} }
const noteIds = notifications const noteIds = notifications
.filter((notification): notification is FilterUnionByProperty<MiNotification, 'type', 'mention' | 'reply' | 'quote'> => ['mention', 'reply', 'quote'].includes(notification.type)) .filter((notification): notification is FilterUnionByProperty<MiNotification, 'type', 'mention' | 'reply' | 'quote' | 'edited'> => ['mention', 'reply', 'quote', 'edited'].includes(notification.type))
.map(notification => notification.noteId); .map(notification => notification.noteId);
if (noteIds.length > 0) { if (noteIds.length > 0) {

View file

@ -20,7 +20,7 @@ export function genOpenapiSpec(config: Config, includeSelfRef = false) {
externalDocs: { externalDocs: {
description: 'Repository', description: 'Repository',
url: 'https://github.com/misskey-dev/misskey', url: 'https://activitypub.software/TransFem-org/Sharkey',
}, },
servers: [{ servers: [{
@ -98,7 +98,7 @@ export function genOpenapiSpec(config: Config, includeSelfRef = false) {
description: desc, description: desc,
externalDocs: { externalDocs: {
description: 'Source code', description: 'Source code',
url: `https://github.com/misskey-dev/misskey/blob/develop/packages/backend/src/server/api/endpoints/${endpoint.name}.ts`, url: `https://activitypub.software/TransFem-org/Sharkey/-/tree/develop/packages/backend/src/server/api/endpoints/${endpoint.name}.ts`,
}, },
...(endpoint.meta.tags ? { ...(endpoint.meta.tags ? {
tags: [endpoint.meta.tags[0]], tags: [endpoint.meta.tags[0]],

View file

@ -235,7 +235,7 @@ export async function mainBoot() {
fetchInstance().then(() => { fetchInstance().then(() => {
const modifiedVersionMustProminentlyOfferInAgplV3Section13Read = miLocalStorage.getItem('modifiedVersionMustProminentlyOfferInAgplV3Section13Read'); const modifiedVersionMustProminentlyOfferInAgplV3Section13Read = miLocalStorage.getItem('modifiedVersionMustProminentlyOfferInAgplV3Section13Read');
if (modifiedVersionMustProminentlyOfferInAgplV3Section13Read !== 'true' && instance.repositoryUrl !== 'https://github.com/misskey-dev/misskey') { if (modifiedVersionMustProminentlyOfferInAgplV3Section13Read !== 'true' && instance.repositoryUrl !== 'https://activitypub.software/TransFem-org/Sharkey/') {
popup(defineAsyncComponent(() => import('@/components/MkSourceCodeAvailablePopup.vue')), {}, {}, 'closed'); popup(defineAsyncComponent(() => import('@/components/MkSourceCodeAvailablePopup.vue')), {}, {}, 'closed');
} }
}); });

View file

@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template> <template>
<div :class="$style.root"> <div :class="$style.root">
<div :class="$style.head"> <div :class="$style.head">
<MkAvatar v-if="['pollEnded', 'note'].includes(notification.type) && notification.note" :class="$style.icon" :user="notification.note.user" link preview/> <MkAvatar v-if="['pollEnded', 'note', 'edited'].includes(notification.type) && notification.note" :class="$style.icon" :user="notification.note.user" link preview/>
<MkAvatar v-else-if="['roleAssigned', 'achievementEarned'].includes(notification.type)" :class="$style.icon" :user="$i" link preview/> <MkAvatar v-else-if="['roleAssigned', 'achievementEarned'].includes(notification.type)" :class="$style.icon" :user="$i" link preview/>
<div v-else-if="notification.type === 'reaction:grouped'" :class="[$style.icon, $style.icon_reactionGroup]"><i class="ph-smiley ph-bold ph-lg" style="line-height: 1;"></i></div> <div v-else-if="notification.type === 'reaction:grouped'" :class="[$style.icon, $style.icon_reactionGroup]"><i class="ph-smiley ph-bold ph-lg" style="line-height: 1;"></i></div>
<div v-else-if="notification.type === 'renote:grouped'" :class="[$style.icon, $style.icon_renoteGroup]"><i class="ph-rocket-launch ph-bold ph-lg" style="line-height: 1;"></i></div> <div v-else-if="notification.type === 'renote:grouped'" :class="[$style.icon, $style.icon_renoteGroup]"><i class="ph-rocket-launch ph-bold ph-lg" style="line-height: 1;"></i></div>
@ -25,8 +25,9 @@ SPDX-License-Identifier: AGPL-3.0-only
[$style.t_pollEnded]: notification.type === 'pollEnded', [$style.t_pollEnded]: notification.type === 'pollEnded',
[$style.t_achievementEarned]: notification.type === 'achievementEarned', [$style.t_achievementEarned]: notification.type === 'achievementEarned',
[$style.t_roleAssigned]: notification.type === 'roleAssigned' && notification.role.iconUrl == null, [$style.t_roleAssigned]: notification.type === 'roleAssigned' && notification.role.iconUrl == null,
[$style.t_pollEnded]: notification.type === 'edited',
}]" }]"
> > <!-- we re-use t_pollEnded for "edited" instead of making an identical style -->
<i v-if="notification.type === 'follow'" class="ph-plus ph-bold ph-lg"></i> <i v-if="notification.type === 'follow'" class="ph-plus ph-bold ph-lg"></i>
<i v-else-if="notification.type === 'receiveFollowRequest'" class="ph-clock ph-bold ph-lg"></i> <i v-else-if="notification.type === 'receiveFollowRequest'" class="ph-clock ph-bold ph-lg"></i>
<i v-else-if="notification.type === 'followRequestAccepted'" class="ph-check ph-bold ph-lg"></i> <i v-else-if="notification.type === 'followRequestAccepted'" class="ph-check ph-bold ph-lg"></i>
@ -40,6 +41,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<img v-if="notification.role.iconUrl" style="height: 1.3em; vertical-align: -22%;" :src="notification.role.iconUrl" alt=""/> <img v-if="notification.role.iconUrl" style="height: 1.3em; vertical-align: -22%;" :src="notification.role.iconUrl" alt=""/>
<i v-else class="ph-seal-check ph-bold ph-lg"></i> <i v-else class="ph-seal-check ph-bold ph-lg"></i>
</template> </template>
<i v-else-if="notification.type === 'edited'" class="ph-pencil ph-bold ph-lg"></i>
<!-- notification.reaction null になることはまずないがここでoptional chaining使うと一部ブラウザで刺さるので念の為 --> <!-- notification.reaction null になることはまずないがここでoptional chaining使うと一部ブラウザで刺さるので念の為 -->
<MkReactionIcon <MkReactionIcon
v-else-if="notification.type === 'reaction'" v-else-if="notification.type === 'reaction'"
@ -61,6 +63,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<span v-else-if="notification.type === 'reaction:grouped'">{{ i18n.tsx._notification.reactedBySomeUsers({ n: notification.reactions.length }) }}</span> <span v-else-if="notification.type === 'reaction:grouped'">{{ i18n.tsx._notification.reactedBySomeUsers({ n: notification.reactions.length }) }}</span>
<span v-else-if="notification.type === 'renote:grouped'">{{ i18n.tsx._notification.renotedBySomeUsers({ n: notification.users.length }) }}</span> <span v-else-if="notification.type === 'renote:grouped'">{{ i18n.tsx._notification.renotedBySomeUsers({ n: notification.users.length }) }}</span>
<span v-else-if="notification.type === 'app'">{{ notification.header }}</span> <span v-else-if="notification.type === 'app'">{{ notification.header }}</span>
<span v-else-if="notification.type === 'edited'">{{ i18n.ts._notification.edited }}</span>
<MkTime v-if="withTime" :time="notification.createdAt" :class="$style.headerTime"/> <MkTime v-if="withTime" :time="notification.createdAt" :class="$style.headerTime"/>
</header> </header>
<div> <div>
@ -131,6 +134,12 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkAvatar :class="$style.reactionsItemAvatar" :user="user" link preview/> <MkAvatar :class="$style.reactionsItemAvatar" :user="user" link preview/>
</div> </div>
</div> </div>
<MkA v-else-if="notification.type === 'edited'" :class="$style.text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
<i class="ph-quotes ph-bold ph-lg" :class="$style.quote"></i>
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="true" :author="notification.note.user"/>
<i class="ph-quotes ph-bold ph-lg" :class="$style.quote"></i>
</MkA>
</div> </div>
</div> </div>
</div> </div>

View file

@ -71,6 +71,7 @@ function close() {
width: calc(100% - (var(--margin) * 2)); width: calc(100% - (var(--margin) * 2));
max-width: 500px; max-width: 500px;
display: flex; display: flex;
backdrop-filter: var(--blur, blur(15px));
} }
.icon { .icon {

View file

@ -27,6 +27,39 @@ SPDX-License-Identifier: AGPL-3.0-only
<div v-if="$i != null" style="text-align: center;"> <div v-if="$i != null" style="text-align: center;">
<MkButton primary rounded inline @click="iLoveMisskey">I <Mfm text="$[jelly ❤]"/> #Sharkey</MkButton> <MkButton primary rounded inline @click="iLoveMisskey">I <Mfm text="$[jelly ❤]"/> #Sharkey</MkButton>
</div> </div>
<FormSection v-if="instance.repositoryUrl !== 'https://activitypub.software/TransFem-org/Sharkey/'">
<div class="_gaps_s">
<MkInfo>
{{ i18n.tsx._aboutMisskey.thisIsModifiedVersion({ name: instance.name }) }}
</MkInfo>
<FormLink v-if="instance.repositoryUrl" :to="instance.repositoryUrl" external>
<template #icon><i class="ph-code ph-bold ph-lg"></i></template>
{{ i18n.ts._aboutMisskey.source }}
</FormLink>
<FormLink v-if="instance.providesTarball" :to="`/tarball/sharkey-${version}.tar.gz`" external>
<template #icon><i class="ph-download ph-bold ph-lg"></i></template>
{{ i18n.ts._aboutMisskey.source }}
<template #suffix>Tarball</template>
</FormLink>
<MkInfo v-if="!instance.repositoryUrl && !instance.providesTarball" warn>
{{ i18n.ts.sourceCodeIsNotYetProvided }}
</MkInfo>
</div>
</FormSection>
<FormSection>
<div class="_gaps_s">
<FormLink to="https://activitypub.software/TransFem-org/Sharkey/" external>
<template #icon><i class="ph-code ph-bold ph-lg"></i></template>
{{ i18n.ts._aboutMisskey.source }} ({{ i18n.ts._aboutMisskey.original_sharkey }})
<template #suffix>GitLab</template>
</FormLink>
<FormLink to="https://ko-fi.com/transfem" external>
<template #icon><i class="ph-piggy-bank ph-bold ph-lg"></i></template>
{{ i18n.ts._aboutMisskey.donate_sharkey }}
<template #suffix>Ko-Fi</template>
</FormLink>
</div>
</FormSection>
<FormSection> <FormSection>
<div class="_gaps_s"> <div class="_gaps_s">
<FormLink to="https://github.com/misskey-dev/misskey" external> <FormLink to="https://github.com/misskey-dev/misskey" external>
@ -41,31 +74,6 @@ SPDX-License-Identifier: AGPL-3.0-only
</FormLink> </FormLink>
</div> </div>
</FormSection> </FormSection>
<FormSection v-if="instance.repositoryUrl !== 'https://github.com/misskey-dev/misskey'">
<div class="_gaps_s">
<MkInfo>
{{ i18n.tsx._aboutMisskey.thisIsModifiedVersion({ name: instance.name }) }}
</MkInfo>
<FormLink v-if="instance.repositoryUrl" :to="instance.repositoryUrl" external>
<template #icon><i class="ph-code ph-bold ph-lg"></i></template>
{{ i18n.ts._aboutMisskey.source }}
<template #suffix>GitLab</template>
</FormLink>
<FormLink to="https://ko-fi.com/transfem" external>
<template #icon><i class="ph-piggy-bank ph-bold ph-lg"></i></template>
{{ i18n.ts._aboutMisskey.donate }}
<template #suffix>Ko-Fi</template>
</FormLink>
<FormLink v-if="instance.providesTarball" :to="`/tarball/sharkey-${version}.tar.gz`" external>
<template #icon><i class="ph-download ph-bold ph-lg"></i></template>
{{ i18n.ts._aboutMisskey.source }}
<template #suffix>Tarball</template>
</FormLink>
<MkInfo v-if="!instance.repositoryUrl && !instance.providesTarball" warn>
{{ i18n.ts.sourceCodeIsNotYetProvided }}
</MkInfo>
</div>
</FormSection>
<FormSection> <FormSection>
<template #label>{{ i18n.ts._aboutMisskey.projectMembers }}</template> <template #label>{{ i18n.ts._aboutMisskey.projectMembers }}</template>
<div :class="$style.contributors" style="margin-bottom: 8px;"> <div :class="$style.contributors" style="margin-bottom: 8px;">

View file

@ -3,21 +3,26 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
const enRegex1 = /(?<=n)a/gi;
const enRegex2 = /(?<=morn)ing/gi;
const enRegex3 = /(?<=every)one/gi;
const koRegex1 = /[나-낳]/g; const koRegex1 = /[나-낳]/g;
const koRegex2 = /(다$)|(다(?=\.))|(다(?= ))|(다(?=!))|(다(?=\?))/gm; const koRegex2 = /(다$)|(다(?=\.))|(다(?= ))|(다(?=!))|(다(?=\?))/gm;
const koRegex3 = /(야(?=\?))|(야$)|(야(?= ))/gm; const koRegex3 = /(야(?=\?))|(야$)|(야(?= ))/gm;
function ifAfter(prefix, fn) {
const preLen = prefix.length;
const regex = new RegExp(prefix,'i');
return (x,pos,string) => {
return pos > 0 && string.substring(pos-preLen,pos).match(regex) ? fn(x) : x;
};
}
export function nyaize(text: string): string { export function nyaize(text: string): string {
return text return text
// ja-JP // ja-JP
.replaceAll('な', 'にゃ').replaceAll('ナ', 'ニャ').replaceAll('ナ', 'ニャ') .replaceAll('な', 'にゃ').replaceAll('ナ', 'ニャ').replaceAll('ナ', 'ニャ')
// en-US // en-US
.replace(enRegex1, x => x === 'A' ? 'YA' : 'ya') .replace(/a/gi, ifAfter('n', x => x === 'A' ? 'YA' : 'ya'))
.replace(enRegex2, x => x === 'ING' ? 'YAN' : 'yan') .replace(/ing/gi, ifAfter('morn', x => x === 'ING' ? 'YAN' : 'yan'))
.replace(enRegex3, x => x === 'ONE' ? 'NYAN' : 'nyan') .replace(/one/gi, ifAfter('every', x => x === 'ONE' ? 'NYAN' : 'nyan'))
// ko-KR // ko-KR
.replace(koRegex1, match => String.fromCharCode( .replace(koRegex1, match => String.fromCharCode(
match.charCodeAt(0)! + '냐'.charCodeAt(0) - '나'.charCodeAt(0), match.charCodeAt(0)! + '냐'.charCodeAt(0) - '나'.charCodeAt(0),

View file

@ -0,0 +1,32 @@
import { describe, test, assert, afterEach } from 'vitest';
import { nyaize } from '@/scripts/nyaize.js';
function runTests(cases) {
for (const c of cases) {
const [input,expected] = c;
const got = nyaize(input);
assert.strictEqual(got, expected);
}
}
describe('nyaize', () => {
test('ja-JP', () => {
runTests([
['きれいな','きれいにゃ'],
['ナナナ', 'ニャニャニャ'],
['ナナ','ニャニャ'],
]);
});
test('en-US', () => {
runTests([
['bar','bar'],
['banana','banyanya'],
['booting','booting'],
['morning','mornyan'],
['mmmorning','mmmornyan'],
['someone','someone'],
['everyone','everynyan'],
['foreveryone','foreverynyan'],
]);
});
});

View file

@ -4273,6 +4273,17 @@ export type components = {
body: string; body: string;
header: string; header: string;
icon: string; icon: string;
} | {
/** Format: id */
id: string;
/** Format: date-time */
createdAt: string;
/** @enum {string} */
type: 'edited';
user: components['schemas']['UserLite'];
/** Format: id */
userId: string;
note: components['schemas']['Note'];
} | { } | {
/** Format: id */ /** Format: id */
id: string; id: string;
@ -19511,7 +19522,7 @@ export type operations = {
url: string; url: string;
/** @default */ /** @default */
secret?: string; secret?: string;
on: ('mention' | 'unfollow' | 'follow' | 'followed' | 'note' | 'reply' | 'renote' | 'reaction')[]; on: ('mention' | 'unfollow' | 'follow' | 'followed' | 'note' | 'reply' | 'renote' | 'reaction' | 'edited')[];
}; };
}; };
}; };
@ -19525,7 +19536,7 @@ export type operations = {
/** Format: misskey:id */ /** Format: misskey:id */
userId: string; userId: string;
name: string; name: string;
on: ('mention' | 'unfollow' | 'follow' | 'followed' | 'note' | 'reply' | 'renote' | 'reaction')[]; on: ('mention' | 'unfollow' | 'follow' | 'followed' | 'note' | 'reply' | 'renote' | 'reaction' | 'edited')[];
url: string; url: string;
secret: string; secret: string;
active: boolean; active: boolean;
@ -19584,7 +19595,7 @@ export type operations = {
/** Format: misskey:id */ /** Format: misskey:id */
userId: string; userId: string;
name: string; name: string;
on: ('mention' | 'unfollow' | 'follow' | 'followed' | 'note' | 'reply' | 'renote' | 'reaction')[]; on: ('mention' | 'unfollow' | 'follow' | 'followed' | 'note' | 'reply' | 'renote' | 'reaction' | 'edited')[];
url: string; url: string;
secret: string; secret: string;
active: boolean; active: boolean;
@ -19651,7 +19662,7 @@ export type operations = {
/** Format: misskey:id */ /** Format: misskey:id */
userId: string; userId: string;
name: string; name: string;
on: ('mention' | 'unfollow' | 'follow' | 'followed' | 'note' | 'reply' | 'renote' | 'reaction')[]; on: ('mention' | 'unfollow' | 'follow' | 'followed' | 'note' | 'reply' | 'renote' | 'reaction' | 'edited')[];
url: string; url: string;
secret: string; secret: string;
active: boolean; active: boolean;
@ -19709,7 +19720,7 @@ export type operations = {
url: string; url: string;
/** @default */ /** @default */
secret?: string; secret?: string;
on: ('mention' | 'unfollow' | 'follow' | 'followed' | 'note' | 'reply' | 'renote' | 'reaction')[]; on: ('mention' | 'unfollow' | 'follow' | 'followed' | 'note' | 'reply' | 'renote' | 'reaction' | 'edited')[];
active: boolean; active: boolean;
}; };
}; };

View file

@ -1,4 +1,4 @@
export const notificationTypes = ['note', 'follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app', 'roleAssigned', 'achievementEarned'] as const; export const notificationTypes = ['note', 'follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app', 'roleAssigned', 'achievementEarned', 'edited'] as const;
export const noteVisibilities = ['public', 'home', 'followers', 'specified'] as const; export const noteVisibilities = ['public', 'home', 'followers', 'specified'] as const;

View file

@ -56,6 +56,7 @@ export type Channels = {
readAntenna: (payload: Antenna) => void; readAntenna: (payload: Antenna) => void;
receiveFollowRequest: (payload: User) => void; receiveFollowRequest: (payload: User) => void;
announcementCreated: (payload: AnnouncementCreated) => void; announcementCreated: (payload: AnnouncementCreated) => void;
edited: (payload: Note) => void;
}; };
receives: null; receives: null;
}; };

View file

@ -232,6 +232,14 @@ async function composeNotification(data: PushNotificationDataMap[keyof PushNotif
data, data,
}]; }];
case 'edited':
return [t('_notification.edited', { name: getUserName(data.body.user) }), {
body: data.body.note.text ?? '',
icon: data.body.user.avatarUrl,
badge: iconUrl('messages'),
data,
}];
default: default:
return null; return null;
} }

View file

@ -133,6 +133,9 @@ globalThis.addEventListener('notificationclick', (ev: ServiceWorkerGlobalScopeEv
case 'showFollowRequests': case 'showFollowRequests':
client = await swos.openClient('push', '/my/follow-requests', loginId); client = await swos.openClient('push', '/my/follow-requests', loginId);
break; break;
case 'edited':
if ('note' in data.body) client = await swos.openPost({ reply: data.body.note }, loginId);
break;
default: default:
switch (data.body.type) { switch (data.body.type) {
case 'receiveFollowRequest': case 'receiveFollowRequest':