Merge branch 'develop' into disable-clipping-of-decoration-in-navbar

This commit is contained in:
zyoshoka 2023-11-11 11:50:32 +09:00 committed by GitHub
commit e9797c9023
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
51 changed files with 314 additions and 85 deletions

View File

@ -9,3 +9,9 @@ body:
description: Tell us what the suggestion is
validations:
required: true
- type: textarea
attributes:
label: Purpose
description: Describe the specific problem or need you think this feature will solve, and who it will help.
validations:
required: true

View File

@ -12,7 +12,19 @@
-->
## 2023.11.0 (unreleased)
## 2023.x.x (unreleased)
### General
-
### Client
-
### Server
- Fix: トークンのないプラグインをアンインストールするときにエラーが出ないように
- Fix: 投稿通知がオンでもダイレクト投稿はユーザーに通知されないようにされました
## 2023.11.0
### Note
- iOS 16.4未満を使用している場合はiOS 16.4以上にアップデートをお願いします
@ -29,6 +41,7 @@
- ユーザーが誤ったメールアドレスを入力した場合に招待コードが失効してしまう問題が解消されます。
- Enhance: すでにフォローしたすべての人の返信をTLに追加できるように
- Enhance: 未読の通知数を表示できるように
- Enhance: 通知されず、確認の必要もないお知らせ(silence)を作成可能になりました
- Enhance: ローカリゼーションの更新
- Enhance: 依存関係の更新
- Change: CWを使用する場合、注釈を空にすることは許可されなくなりました
@ -50,7 +63,7 @@
- Enhance: プラグインで`Plugin:register_note_view_interruptor`を用いてnoteの代わりにnullを返却することでートを非表示にできるようになりました
- Enhance: AiScript関数`Mk:nyaize()`が追加されました
- Enhance: 情報→ツール はナビゲーションバーにツールとして独立した項目になりました
- Enhance: ノート内のカスタム絵文字をクリックすることで、コピーおよびリアクションができるように
- Enhance: ノート内の絵文字をクリックすることで、コピーおよびリアクションができるように
- Enhance: その他細かなブラッシュアップ
- Fix: 投稿フォームでのユーザー変更がプレビューに反映されない問題を修正
- Fix: ユーザーページの ノート > ファイル付き タブにリプライが表示されてしまう
@ -63,6 +76,7 @@
- Fix: 11以上されているリアクションにおいてツールチップで示されるリアクション数が本来よりも1多い問題を修正 #12174
- Fix: サイレンス状態で公開範囲のパブリックを選択できてしまう問題を修正 #12224
- Fix: In deck layout, replies option is not saved after refresh
- Fix: アーカイブしたお知らせがコントロールパネルに表示される問題を修正
- Note: アップデート後、サウンドに関する設定が初期化されます
### Server
@ -72,6 +86,7 @@
- Enhance: プロフィールの自己紹介欄のMFMが連合するようになりました
- 相手がMisskey v2023.11.0以降である必要があります
- Enhance: チャンネル取得時のパフォーマンスを向上
- Enhance: AP: ApplicationタイプのアカウントをisBotとして扱うように
- Fix: リストTLに自分のフォロワー限定投稿が含まれない問題を修正
- Fix: ローカルタイムラインに投稿者自身の投稿への返信が含まれない問題を修正
- Fix: 自分のフォローしているユーザーの自分のフォローしていないユーザーの visibility: followers な投稿への返信がストリーミングで流れてくる問題を修正

View File

@ -1157,6 +1157,7 @@ disableStreamingTimeline: "Disable real-time timeline updates"
useGroupedNotifications: "Display grouped notifications"
signupPendingError: "There was a problem verifying the email address. The link may have expired."
cwNotationRequired: "If \"Hide content\" is enabled, a description must be provided."
doReaction: "Add reaction"
_announcement:
forExistingUsers: "Existing users only"
forExistingUsersDescription: "This announcement will only be shown to users existing at the point of publishment if enabled. If disabled, those newly signing up after it has been posted will also see it."
@ -1203,6 +1204,54 @@ _initialTutorial:
title: "What are Reactions?"
description: "Notes can be reacted to with various emojis. Reactions allow you to express nuances that may not be conveyed with just a 'like.'"
letsTryReacting: "Reactions can be added by clicking the '+' button on the note. Try reacting to this sample note!"
reactToContinue: "Add a reaction to proceed."
reactNotification: "You'll receive real-time notifications when someone reacts to your note."
reactDone: "You can undo a reaction by pressing the '-' button."
_timeline:
title: "The Concept of Timelines"
description1: "Misskey provides multiple timelines based on usage (some may not be available depending on the server's policies)."
home: "You can view notes from accounts you follow."
local: "You can view notes from all users on this server."
social: "Notes from the Home and Local timelines will be displayed."
global: "You can view notes from all connected servers."
description2: "You can switch between timelines at the top of the screen at any time."
description3: "Additionally, there are list timelines and channel timelines. For more details, please refer to {link}."
_postNote:
title: "Note Posting Settings"
description1: "When posting a note on Misskey, various options are available. The posting form looks like this."
_visibility:
description: "You can limit who can view your note."
public: "Your note will be visible for all users."
home: "Public only on the Home timeline. People visiting your profile, via followers, and through renotes can see it."
followers: "Visible to followers only. Only followers can see it and no one else, and it cannot be renoted by others."
direct: "Visible only to specified users, and the recipient will be notified. It can be used as an alternative to direct messaging."
doNotSendConfidencialOnDirect1: "Be careful when sending sensitive information!"
doNotSendConfidencialOnDirect2: "Administrators of the server can see what you write. Be careful with sensitive information when sending direct notes to users on untrusted servers."
localOnly: "Posting with this flag will not federate the note to other servers. Users on other servers will not be able to view these notes directly, regardless of the display settings above."
_cw:
title: "Content Warning"
description: "Instead of the body, the content written in 'comments' field will be displayed. Pressing \"read more\" will reveal the body."
_exampleNote:
cw: "This will surely make you hungry!"
note: "Just had a chocolate-glazed donut 🍩😋"
useCases: "This is used when following the server guidelines for necessary notes or for self-restriction of spoiler or sensitive text."
_howToMakeAttachmentsSensitive:
title: "How to Mark Attachments as Sensitive?"
description: "For attachments that are required by server guidelines or that should not be left intact, add a \"sensitive\" flag."
tryThisFile: "Try marking the image attached in this form as sensitive!"
_exampleNote:
note: "Oops, messed up opening the natto lid..."
method: "To mark an attachment as sensitive, click the file thumbnail, open the menu, and click \"Mark as Sensitive.\""
sensitiveSucceeded: "When attaching files, please set sensitivities in accordance with the server guidelines."
doItToContinue: "Mark the attachment file as sensitive to proceed."
_done:
title: "The tutorial is complete! 🎉"
description: "The functions introduced here are just a small part. For a more detailed understanding of using Misskey, please refer to {link}."
_timelineDescription:
home: "In the Home timeline, you can see notes from accounts you follow."
local: "In the Local timeline, you can see notes from all users on this server."
social: "The Social timeline displays notes from both the Home and Local timelines."
global: "In the Global timeline, you can see notes from all connected servers."
_serverRules:
description: "A set of rules to be displayed before registration. Setting a summary of the Terms of Service is recommended."
_serverSettings:
@ -1472,6 +1521,9 @@ _achievements:
_smashTestNotificationButton:
title: "Test overflow"
description: "Trigger the notification test repeatedly within an extremely short time"
_tutorialCompleted:
title: "Misskey Elementary Course Diploma"
description: "Tutorial completed"
_role:
new: "New role"
edit: "Edit role"
@ -1660,6 +1712,7 @@ _channel:
notesCount: "{n} Notes"
nameAndDescription: "Name and description"
nameOnly: "Name only"
allowRenoteToExternal: "Allow renote and quote outside the channel"
_menuDisplay:
sideFull: "Side"
sideIcon: "Side (Icons)"

View File

@ -1088,7 +1088,26 @@ _initialAccountSetting:
profileSetting: "Paramètres du profil"
privacySetting: "Paramètres de confidentialité"
initialAccountSettingCompleted: "Configuration du profil terminée avec succès !"
startTutorial: "Démarrer le tutoriel"
skipAreYouSure: "Désirez-vous ignorer la configuration du profil ?"
_initialTutorial:
title: "Tutoriel"
wellDone: "Bien joué !"
skipAreYouSure: "Quitter le tutoriel ?"
_landing:
title: "Bienvenue dans le tutoriel"
description: "Ici, vous pouvez apprendre l'utilisation de base de Misskey et ses fonctionnalités."
_note:
title: "Qu'est-ce que les notes ?"
description: "Les messages sur Misskey sont appelés des « notes » . Les notes sont classées par ordre chronologique sur le fil et sont mises à jour en temps réel."
reply: "Vous pouvez répondre aux messages. Vous pouvez également répondre aux réponses et poursuivre la conversation comme un fil de discussion."
renote: "Vous pouvez partager cette note sur votre propre fil. Vous pouvez aussi ajouter du texte en citant."
reaction: "Vous pouvez ajouter des réactions. Les détails sont expliqués à la page suivante."
menu: "Vous pouvez afficher les détails de la note, copier le lien et effectuer d'autres actions."
_reaction:
title: "Qu'est-ce que les réactions ?"
description: "Vous pouvez ajouter des « réactions » aux notes. Les réactions vous permettent d'exprimer à l'aise des nuances qui ne peuvent pas être exprimées par des mentions j'aime."
letsTryReacting: "Des réactions peuvent être ajoutées en cliquant sur le bouton « + » de la note. Essayez d'ajouter une réaction à cet exemple de note !"
_serverSettings:
iconUrl: "URL de licône"
fanoutTimelineDescription: "Si activée, la performance de la récupération de la chronologie augmentera considérablement et la charge sur la base de données sera réduite. En revanche, l'utilisation de la mémoire de Redis augmentera. Considérez désactiver cette option si le serveur est bas en mémoire ou instable."

2
locales/index.d.ts vendored
View File

@ -1172,6 +1172,8 @@ export interface Locale {
"readConfirmText": string;
"shouldNotBeUsedToPresentPermanentInfo": string;
"dialogAnnouncementUxWarn": string;
"silence": string;
"silenceDescription": string;
};
"_initialAccountSetting": {
"accountCreated": string;

View File

@ -53,6 +53,19 @@ const clean = (text) => text.replace(new RegExp(String.fromCodePoint(0x08), 'g')
const locales = languages.reduce((a, c) => (a[c] = yaml.load(clean(fs.readFileSync(new URL(`${c}.yml`, import.meta.url), 'utf-8'))) || {}, a), {});
// 空文字列が入ることがあり、フォールバックが動作しなくなるのでプロパティごと消す
const removeEmpty = (obj) => {
for (const [k, v] of Object.entries(obj)) {
if (v === '') {
delete obj[k];
} else if (typeof v === 'object') {
removeEmpty(v);
}
}
return obj;
};
removeEmpty(locales);
export default Object.entries(locales)
.reduce((a, [k ,v]) => (a[k] = (() => {
const [lang] = k.split('-');
@ -63,7 +76,7 @@ export default Object.entries(locales)
default: return merge(
locales['ja-JP'],
locales['en-US'],
locales[`${lang}-${primaries[lang]}`] || {},
locales[`${lang}-${primaries[lang]}`] ?? {},
v
);
}

View File

@ -1157,6 +1157,7 @@ disableStreamingTimeline: "Disabilitare gli aggiornamenti della TL in tempo real
useGroupedNotifications: "Mostra le notifiche raggruppate"
signupPendingError: "Si è verificato un problema durante la verifica del tuo indirizzo email. Potrebbe essere scaduto il collegamento temporaneo."
cwNotationRequired: "Devi indicare perché il contenuto è indicato come esplicito."
doReaction: "Reagisci"
_announcement:
forExistingUsers: "Solo ai profili attuali"
forExistingUsersDescription: "L'annuncio sarà visibile solo ai profili esistenti in questo momento. Se disabilitato, sarà visibile anche ai profili che verranno creati dopo la pubblicazione di questo annuncio."

View File

@ -1170,6 +1170,8 @@ _announcement:
readConfirmText: "「{title}」の内容を読み、既読にします。"
shouldNotBeUsedToPresentPermanentInfo: "特に新規ユーザーのUXを損ねる可能性が高いため、ストック情報ではなくフロー情報の掲示にお知らせを使用することを推奨します。"
dialogAnnouncementUxWarn: "ダイアログ形式のお知らせが同時に2つ以上ある場合、UXに悪影響を及ぼす可能性が非常に高いため、使用は慎重に行うことを推奨します。"
silence: "非通知"
silenceDescription: "オンにすると、このお知らせは通知されず、既読にする必要もなくなります。"
_initialAccountSetting:
accountCreated: "アカウントの作成が完了しました!"

View File

@ -1157,6 +1157,7 @@ disableStreamingTimeline: "停用時間軸的即時更新"
useGroupedNotifications: "分組顯示通知訊息"
signupPendingError: "驗證您的電子郵件地址時出現問題。連結可能已過期。"
cwNotationRequired: "如果開啟「隱藏內容」,則需要註解說明。"
doReaction: "做出反應"
_announcement:
forExistingUsers: "僅限既有的使用者"
forExistingUsersDescription: "啟用代表僅向現存使用者顯示;停用代表張貼後註冊的新使用者也會看到。"
@ -1229,7 +1230,7 @@ _initialTutorial:
localOnly: "不將貼文發布到聯邦上的其他伺服器。不論上述發布範圍,使用此設定後,其他伺服器上的使用者將無法直接查看此貼文。"
_cw:
title: "隱藏內容CW"
description: "將顯示「註釋」中寫入的內容而不是本文。按一下「查看更多」以顯示本文。"
description: "將顯示「註釋」中寫入的內容而不是本文。按一下「顯示內容」以顯示本文。"
_exampleNote:
cw: "美食恐怖主義注意"
note: "我吃了一個巧克力甜甜圈🍩😋"

View File

@ -1,6 +1,6 @@
{
"name": "misskey",
"version": "2023.11.0-beta.10",
"version": "2023.11.0",
"codename": "nasubi",
"repository": {
"type": "git",

View File

@ -0,0 +1,18 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class AnnouncementSilence1699141698112 {
name = 'AnnouncementSilence1699141698112'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "announcement" ADD "silence" boolean NOT NULL DEFAULT false`);
await queryRunner.query(`CREATE INDEX "IDX_7b8d9225168e962f94ea517e00" ON "announcement" ("silence") `);
}
async down(queryRunner) {
await queryRunner.query(`DROP INDEX "public"."IDX_7b8d9225168e962f94ea517e00"`);
await queryRunner.query(`ALTER TABLE "announcement" DROP COLUMN "silence"`);
}
}

View File

@ -47,6 +47,7 @@ export class AnnouncementService {
const q = this.announcementsRepository.createQueryBuilder('announcement')
.where('announcement.isActive = true')
.andWhere('announcement.silence = false')
.andWhere(new Brackets(qb => {
qb.orWhere('announcement.userId = :userId', { userId: user.id });
qb.orWhere('announcement.userId IS NULL');
@ -73,6 +74,7 @@ export class AnnouncementService {
icon: values.icon,
display: values.display,
forExistingUsers: values.forExistingUsers,
silence: values.silence,
needConfirmationToRead: values.needConfirmationToRead,
userId: values.userId,
}).then(x => this.announcementsRepository.findOneByOrFail(x.identifiers[0]));
@ -124,6 +126,7 @@ export class AnnouncementService {
display: values.display,
icon: values.icon,
forExistingUsers: values.forExistingUsers,
silence: values.silence,
needConfirmationToRead: values.needConfirmationToRead,
isActive: values.isActive,
});
@ -210,6 +213,7 @@ export class AnnouncementService {
icon: announcement.icon,
display: announcement.display,
needConfirmationToRead: announcement.needConfirmationToRead,
silence: announcement.silence,
forYou: announcement.userId === me?.id,
isRead: reads.some(read => read.announcementId === announcement.id),
}));

View File

@ -521,11 +521,13 @@ export class NoteCreateService implements OnApplicationShutdown {
followeeId: user.id,
notify: 'normal',
}).then(followings => {
for (const following of followings) {
// TODO: ワードミュート考慮
this.notificationService.createNotification(following.followerId, 'note', {
noteId: note.id,
}, user.id);
if (note.visibility !== 'specified') {
for (const following of followings) {
// TODO: ワードミュート考慮
this.notificationService.createNotification(following.followerId, 'note', {
noteId: note.id,
}, user.id);
}
}
});
}

View File

@ -269,7 +269,7 @@ export class ApPersonService implements OnModuleInit {
const tags = extractApHashtags(person.tag).map(normalizeForSearch).splice(0, 32);
const isBot = getApType(object) === 'Service';
const isBot = getApType(object) === 'Service' || getApType(object) === 'Application';
const bday = person['vcard:bday']?.match(/^\d{4}-\d{2}-\d{2}/);
@ -456,7 +456,7 @@ export class ApPersonService implements OnModuleInit {
emojis: emojiNames,
name: truncate(person.name, nameLength),
tags,
isBot: getApType(object) === 'Service',
isBot: getApType(object) === 'Service' || getApType(object) === 'Application',
isCat: (person as any).isCat === true,
isLocked: person.manuallyApprovesFollowers,
movedToUri: person.movedTo ?? null,

View File

@ -66,6 +66,12 @@ export class MiAnnouncement {
})
public forExistingUsers: boolean;
@Index()
@Column('boolean', {
default: false,
})
public silence: boolean;
@Index()
@Column({
...id(),

View File

@ -58,6 +58,7 @@ export const paramDef = {
icon: { type: 'string', enum: ['info', 'warning', 'error', 'success'], default: 'info' },
display: { type: 'string', enum: ['normal', 'banner', 'dialog'], default: 'normal' },
forExistingUsers: { type: 'boolean', default: false },
silence: { type: 'boolean', default: false },
needConfirmationToRead: { type: 'boolean', default: false },
userId: { type: 'string', format: 'misskey:id', nullable: true, default: null },
},
@ -78,6 +79,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
icon: ps.icon,
display: ps.display,
forExistingUsers: ps.forExistingUsers,
silence: ps.silence,
needConfirmationToRead: ps.needConfirmationToRead,
userId: ps.userId,
}, me);

View File

@ -86,6 +86,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
) {
super(meta, paramDef, async (ps, me) => {
const query = this.queryService.makePaginationQuery(this.announcementsRepository.createQueryBuilder('announcement'), ps.sinceId, ps.untilId);
query.andWhere('announcement.isActive = true');
if (ps.userId) {
query.andWhere('announcement.userId = :userId', { userId: ps.userId });
} else {
@ -113,6 +114,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
display: announcement.display,
isActive: announcement.isActive,
forExistingUsers: announcement.forExistingUsers,
silence: announcement.silence,
needConfirmationToRead: announcement.needConfirmationToRead,
userId: announcement.userId,
reads: reads.get(announcement)!,

View File

@ -35,6 +35,7 @@ export const paramDef = {
icon: { type: 'string', enum: ['info', 'warning', 'error', 'success'] },
display: { type: 'string', enum: ['normal', 'banner', 'dialog'] },
forExistingUsers: { type: 'boolean' },
silence: { type: 'boolean' },
needConfirmationToRead: { type: 'boolean' },
isActive: { type: 'boolean' },
},
@ -63,6 +64,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
display: ps.display,
icon: ps.icon,
forExistingUsers: ps.forExistingUsers,
silence: ps.silence,
needConfirmationToRead: ps.needConfirmationToRead,
isActive: ps.isActive,
}, me);

View File

@ -18,7 +18,7 @@ export const paramDef = {
type: 'object',
properties: {
tokenId: { type: 'string', format: 'misskey:id' },
token: { type: 'string' },
token: { type: 'string', nullable: true },
},
anyOf: [
{ required: ['tokenId'] },

View File

@ -175,6 +175,7 @@ export class ClientServerService {
serverErrorImageUrl: meta.serverErrorImageUrl ?? 'https://xn--931a.moe/assets/error.jpg',
infoImageUrl: meta.infoImageUrl ?? 'https://xn--931a.moe/assets/info.jpg',
notFoundImageUrl: meta.notFoundImageUrl ?? 'https://xn--931a.moe/assets/not-found.jpg',
instanceUrl: this.config.url,
};
}

View File

@ -26,6 +26,7 @@ html
meta(name='theme-color' content= themeColor || '#86b300')
meta(name='theme-color-orig' content= themeColor || '#86b300')
meta(property='og:site_name' content= instanceName || 'Misskey')
meta(property='instance_url' content= instanceUrl)
meta(name='viewport' content='width=device-width, initial-scale=1')
link(rel='icon' href= icon || '/favicon.ico')
link(rel='apple-touch-icon' href= appleTouchIcon || '/apple-touch-icon.png')

View File

@ -29,7 +29,7 @@
"@vue/compiler-sfc": "3.3.7",
"astring": "1.8.6",
"autosize": "6.0.1",
"aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.0.5",
"aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.0.6",
"broadcast-channel": "6.0.0",
"browser-image-resizer": "github:misskey-dev/browser-image-resizer#v2.2.1-misskey.3",
"buraha": "0.0.1",

View File

@ -8,7 +8,7 @@ import { common } from './common.js';
import { version, ui, lang, updateLocale } from '@/config.js';
import { i18n, updateI18n } from '@/i18n.js';
import { confirm, alert, post, popup, toast } from '@/os.js';
import { useStream, isReloading } from '@/stream.js';
import { useStream } from '@/stream.js';
import * as sound from '@/scripts/sound.js';
import { $i, refreshAccount, login, updateAccount, signout } from '@/account.js';
import { defaultStore, ColdDeviceStorage } from '@/store.js';
@ -39,7 +39,6 @@ export async function mainBoot() {
let reloadDialogShowing = false;
stream.on('_disconnected_', async () => {
if (isReloading) return;
if (defaultStore.state.serverDisconnectedBehavior === 'reload') {
location.reload();
} else if (defaultStore.state.serverDisconnectedBehavior === 'dialog') {
@ -58,7 +57,7 @@ export async function mainBoot() {
});
for (const plugin of ColdDeviceStorage.get('plugins').filter(p => p.active)) {
import('../plugin').then(async ({ install }) => {
import('@/plugin.js').then(async ({ install }) => {
// Workaround for https://bugs.webkit.org/show_bug.cgi?id=242740
await new Promise(r => setTimeout(r, 0));
install(plugin);

View File

@ -202,11 +202,17 @@ let note = $ref(deepClone(props.note));
// plugin
if (noteViewInterruptors.length > 0) {
onMounted(async () => {
let result:Misskey.entities.Note | null = deepClone(note);
let result: Misskey.entities.Note | null = deepClone(note);
for (const interruptor of noteViewInterruptors) {
result = await interruptor.handler(result);
if (result === null) return isDeleted.value = true;
try {
result = await interruptor.handler(result);
if (result === null) {
isDeleted.value = true;
return;
}
} catch (err) {
console.error(err);
}
}
note = result;
});
@ -228,8 +234,8 @@ const clipButton = shallowRef<HTMLElement>();
let appearNote = $computed(() => isRenote ? note.renote as Misskey.entities.Note : note);
const isMyRenote = $i && ($i.id === note.userId);
const showContent = ref(false);
const parsed = appearNote.text ? mfm.parse(appearNote.text) : null;
const urls = parsed ? extractUrlFromMfm(parsed) : null;
const parsed = $computed(() => appearNote.text ? mfm.parse(appearNote.text) : null);
const urls = $computed(() => parsed ? extractUrlFromMfm(parsed) : null);
const isLong = shouldCollapsed(appearNote, urls ?? []);
const collapsed = ref(appearNote.cw == null && isLong);
const isDeleted = ref(false);
@ -298,7 +304,7 @@ function renote(viaKeyboard = false) {
const { menu } = getRenoteMenu({ note: note, renoteButton, mock: props.mock });
os.popupMenu(menu, renoteButton.value, {
viaKeyboard,
}).then(focus);
});
}
function reply(viaKeyboard = false): void {

View File

@ -239,11 +239,17 @@ let note = $ref(deepClone(props.note));
// plugin
if (noteViewInterruptors.length > 0) {
onMounted(async () => {
let result:Misskey.entities.Note | null = deepClone(note);
let result: Misskey.entities.Note | null = deepClone(note);
for (const interruptor of noteViewInterruptors) {
result = await interruptor.handler(result);
if (result === null) return isDeleted.value = true;
try {
result = await interruptor.handler(result);
if (result === null) {
isDeleted.value = true;
return;
}
} catch (err) {
console.error(err);
}
}
note = result;
});
@ -344,7 +350,7 @@ function renote(viaKeyboard = false) {
const { menu } = getRenoteMenu({ note: note, renoteButton });
os.popupMenu(menu, renoteButton.value, {
viaKeyboard,
}).then(focus);
});
}
function reply(viaKeyboard = false): void {

View File

@ -96,6 +96,10 @@ onUnmounted(() => {
onDeactivated(() => {
if (connection) connection.dispose();
});
defineExpose({
reload,
});
</script>
<style lang="scss" module>

View File

@ -750,7 +750,11 @@ async function post(ev?: MouseEvent) {
// plugin
if (notePostInterruptors.length > 0) {
for (const interruptor of notePostInterruptors) {
postData = await interruptor.handler(deepClone(postData));
try {
postData = await interruptor.handler(deepClone(postData));
} catch (err) {
console.error(err);
}
}
}

View File

@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import { computed, provide, onUnmounted } from 'vue';
import MkNotes from '@/components/MkNotes.vue';
import MkPullToRefresh from '@/components/MkPullToRefresh.vue';
import { useStream, reloadStream } from '@/stream.js';
import { useStream } from '@/stream.js';
import * as sound from '@/scripts/sound.js';
import { $i } from '@/account.js';
import { instance } from '@/instance.js';
@ -201,7 +201,6 @@ function reloadTimeline() {
tlNotesCount = 0;
tlComponent.pagingComponent?.reload().then(() => {
reloadStream();
res();
});
});

View File

@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div class="_gaps">
<div style="text-align: center; padding: 0 16px;">{{ i18n.ts._initialTutorial._postNote.description1 }}</div>
<MkPostForm :class="$style.exampleRoot" :mock="true"/>
<MkPostForm :class="$style.exampleRoot" :mock="true" :autofocus="false"/>
<MkFormSection>
<template #label>{{ i18n.ts.visibility }}</template>
<div class="_gaps">

View File

@ -11,6 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkPostForm
:class="$style.exampleRoot"
:mock="true"
:autofocus="false"
:initialNote="exampleNote"
@fileChangeSensitive="doSucceeded"
></MkPostForm>

View File

@ -18,10 +18,10 @@ interface Props {
const contentSymbol = Symbol();
const observer = new ResizeObserver((entries) => {
const results: {
container: HTMLSpanElement;
transform: string;
}[] = [];
const results: {
container: HTMLSpanElement;
transform: string;
}[] = [];
for (const entry of entries) {
const content = (entry.target[contentSymbol] ? entry.target : entry.target.firstElementChild) as HTMLSpanElement;
const props: Required<Props> = content[contentSymbol];

View File

@ -4,21 +4,28 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<img v-if="!useOsNativeEmojis" :class="$style.root" :src="url" :alt="props.emoji" decoding="async" @pointerenter="computeTitle"/>
<span v-else-if="useOsNativeEmojis" :alt="props.emoji" @pointerenter="computeTitle">{{ props.emoji }}</span>
<img v-if="!useOsNativeEmojis" :class="$style.root" :src="url" :alt="props.emoji" decoding="async" @pointerenter="computeTitle" @click="onClick"/>
<span v-else-if="useOsNativeEmojis" :alt="props.emoji" @pointerenter="computeTitle" @click="onClick">{{ props.emoji }}</span>
<span v-else>{{ emoji }}</span>
</template>
<script lang="ts" setup>
import { computed } from 'vue';
import { computed, inject } from 'vue';
import { char2twemojiFilePath, char2fluentEmojiFilePath } from '@/scripts/emoji-base.js';
import { defaultStore } from '@/store.js';
import { getEmojiName } from '@/scripts/emojilist.js';
import * as os from '@/os.js';
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
import { i18n } from '@/i18n.js';
const props = defineProps<{
emoji: string;
menu?: boolean;
menuReaction?: boolean;
}>();
const react = inject<((name: string) => void) | null>('react', null);
const char2path = defaultStore.state.emojiStyle === 'twemoji' ? char2twemojiFilePath : char2fluentEmojiFilePath;
const useOsNativeEmojis = computed(() => defaultStore.state.emojiStyle === 'native');
@ -31,6 +38,28 @@ function computeTitle(event: PointerEvent): void {
const title = getEmojiName(props.emoji as string) ?? props.emoji as string;
(event.target as HTMLElement).title = title;
}
function onClick(ev: MouseEvent) {
if (props.menu) {
os.popupMenu([{
type: 'label',
text: props.emoji,
}, {
text: i18n.ts.copy,
icon: 'ti ti-copy',
action: () => {
copyToClipboard(props.emoji);
os.success();
},
}, ...(props.menuReaction && react ? [{
text: i18n.ts.doReaction,
icon: 'ti ti-plus',
action: () => {
react(props.emoji);
},
}] : [])], ev.currentTarget ?? ev.target);
}
}
</script>
<style lang="scss" module>

View File

@ -354,6 +354,8 @@ export default function(props: MfmProps) {
return [h(MkEmoji, {
key: Math.random(),
emoji: token.props.emoji,
menu: props.enableEmojiMenu,
menuReaction: props.enableEmojiMenuReaction,
})];
}

View File

@ -5,7 +5,7 @@
import { miLocalStorage } from '@/local-storage.js';
const address = new URL(location.href);
const address = new URL(document.querySelector<HTMLMetaElement>('meta[property="instance_url"]')?.content || location.href);
const siteName = document.querySelector<HTMLMetaElement>('meta[property="og:site_name"]')?.content;
export const host = address.host;

View File

@ -48,6 +48,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkSwitch v-model="announcement.forExistingUsers" :helpText="i18n.ts._announcement.forExistingUsersDescription">
{{ i18n.ts._announcement.forExistingUsers }}
</MkSwitch>
<MkSwitch v-model="announcement.silence" :helpText="i18n.ts._announcement.silenceDescription">
{{ i18n.ts._announcement.silence }}
</MkSwitch>
<MkSwitch v-model="announcement.needConfirmationToRead" :helpText="i18n.ts._announcement.needConfirmationToReadDescription">
{{ i18n.ts._announcement.needConfirmationToRead }}
</MkSwitch>
@ -97,6 +100,7 @@ function add() {
icon: 'info',
display: 'normal',
forExistingUsers: false,
silence: false,
needConfirmationToRead: false,
});
}

View File

@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<section v-for="announcement in items" :key="announcement.id" class="_panel" :class="$style.announcement">
<div v-if="announcement.forYou" :class="$style.forYou"><i class="ti ti-pin"></i> {{ i18n.ts.forYou }}</div>
<div :class="$style.header">
<span v-if="$i && !announcement.isRead" style="margin-right: 0.5em;">🆕</span>
<span v-if="$i && !announcement.silence && !announcement.isRead" style="margin-right: 0.5em;">🆕</span>
<span style="margin-right: 0.5em;">
<i v-if="announcement.icon === 'info'" class="ti ti-info-circle"></i>
<i v-else-if="announcement.icon === 'warning'" class="ti ti-alert-triangle" style="color: var(--warn);"></i>
@ -29,7 +29,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkTime :time="announcement.updatedAt ?? announcement.createdAt" mode="detail"/>
</div>
</div>
<div v-if="tab !== 'past' && $i && !announcement.isRead" :class="$style.footer">
<div v-if="tab !== 'past' && $i && !announcement.silence && !announcement.isRead" :class="$style.footer">
<MkButton primary @click="read(announcement)"><i class="ti ti-check"></i> {{ i18n.ts.gotIt }}</MkButton>
</div>
</section>

View File

@ -94,7 +94,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:class="[$style.avatarDecoration, { [$style.avatarDecorationActive]: $i.avatarDecorations.some(x => x.id === avatarDecoration.id) }]"
@click="openDecoration(avatarDecoration)"
>
<div :class="$style.avatarDecorationName"><MkCondensedLine :minScale="2 / 3">{{ avatarDecoration.name }}</MkCondensedLine></div>
<div :class="$style.avatarDecorationName"><MkCondensedLine :minScale="0.5">{{ avatarDecoration.name }}</MkCondensedLine></div>
<MkAvatar style="width: 60px; height: 60px;" :user="$i" :decoration="{ url: avatarDecoration.url }" forceShowDecoration/>
<i v-if="avatarDecoration.roleIdsThatCanBeUsedThisDecoration.length > 0 && !$i.roles.some(r => avatarDecoration.roleIdsThatCanBeUsedThisDecoration.includes(r.id))" :class="$style.avatarDecorationLock" class="ti ti-lock"></i>
</div>

View File

@ -11,10 +11,9 @@ import { Plugin, noteActions, notePostInterruptors, noteViewInterruptors, postFo
const parser = new Parser();
const pluginContexts = new Map<string, Interpreter>();
export function install(plugin: Plugin): void {
export async function install(plugin: Plugin): Promise<void> {
// 後方互換性のため
if (plugin.src == null) return;
console.info('Plugin installed:', plugin.name, 'v' + plugin.version);
const aiscript = new Interpreter(createPluginEnv({
plugin: plugin,
@ -42,7 +41,14 @@ export function install(plugin: Plugin): void {
initPlugin({ plugin, aiscript });
aiscript.exec(parser.parse(plugin.src));
try {
await aiscript.exec(parser.parse(plugin.src));
} catch (err) {
console.error('Plugin install failed:', plugin.name, 'v' + plugin.version);
return;
}
console.info('Plugin installed:', plugin.name, 'v' + plugin.version);
}
function createPluginEnv(opts: { plugin: Plugin; storageKey: string }): Record<string, values.Value> {

View File

@ -9,9 +9,6 @@ import { $i } from '@/account.js';
import { url } from '@/config.js';
let stream: Misskey.Stream | null = null;
let timeoutHeartBeat: number | null = null;
export let isReloading: boolean = false;
export function useStream(): Misskey.Stream {
if (stream) return stream;
@ -20,20 +17,7 @@ export function useStream(): Misskey.Stream {
token: $i.token,
} : null));
timeoutHeartBeat = window.setTimeout(heartbeat, 1000 * 60);
return stream;
}
export function reloadStream() {
if (!stream) return useStream();
if (timeoutHeartBeat) window.clearTimeout(timeoutHeartBeat);
isReloading = true;
stream.close();
stream.once('_connected_', () => isReloading = false);
stream.stream.reconnect();
timeoutHeartBeat = window.setTimeout(heartbeat, 1000 * 60);
window.setTimeout(heartbeat, 1000 * 60);
return stream;
}
@ -42,5 +26,5 @@ function heartbeat(): void {
if (stream != null && document.visibilityState === 'visible') {
stream.heartbeat();
}
timeoutHeartBeat = window.setTimeout(heartbeat, 1000 * 60);
window.setTimeout(heartbeat, 1000 * 60);
}

View File

@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { onUnmounted } from 'vue';
import { useStream, isReloading } from '@/stream.js';
import { useStream } from '@/stream.js';
import { i18n } from '@/i18n.js';
import MkButton from '@/components/MkButton.vue';
import * as os from '@/os.js';
@ -26,7 +26,6 @@ const zIndex = os.claimZIndex('high');
let hasDisconnected = $ref(false);
function onDisconnected() {
if (isReloading) return;
hasDisconnected = true;
}

View File

@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<XColumn :menu="menu" :column="column" :isStacked="isStacked">
<XColumn :menu="menu" :column="column" :isStacked="isStacked" :refresher="() => timeline.reloadTimeline()">
<template #header>
<i class="ti ti-antenna"></i><span style="margin-left: 8px;">{{ column.name }}</span>
</template>

View File

@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<XColumn :menu="menu" :column="column" :isStacked="isStacked">
<XColumn :menu="menu" :column="column" :isStacked="isStacked" :refresher="() => timeline.reloadTimeline()">
<template #header>
<i class="ti ti-device-tv"></i><span style="margin-left: 8px;">{{ column.name }}</span>
</template>

View File

@ -57,6 +57,7 @@ const props = withDefaults(defineProps<{
isStacked?: boolean;
naked?: boolean;
menu?: MenuItem[];
refresher?: () => Promise<void>;
}>(), {
isStacked: false,
naked: false,
@ -183,6 +184,18 @@ function getMenu() {
items = props.menu.concat(items);
}
if (props.refresher) {
items = [{
icon: 'ti ti-refresh',
text: i18n.ts.reload,
action: () => {
if (props.refresher) {
props.refresher();
}
},
}, ...items];
}
return items;
}

View File

@ -4,10 +4,10 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<XColumn :column="column" :isStacked="isStacked">
<XColumn :column="column" :isStacked="isStacked" :refresher="() => reloadTimeline()">
<template #header><i class="ti ti-mail" style="margin-right: 8px;"></i>{{ column.name }}</template>
<MkNotes :pagination="pagination"/>
<MkNotes ref="tlComponent" :pagination="pagination"/>
</XColumn>
</template>
@ -29,4 +29,14 @@ const pagination = {
visibility: 'specified',
},
};
const tlComponent: InstanceType<typeof MkNotes> = $ref();
function reloadTimeline() {
return new Promise<void>((res) => {
tlComponent.pagingComponent?.reload().then(() => {
res();
});
});
}
</script>

View File

@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<XColumn :menu="menu" :column="column" :isStacked="isStacked">
<XColumn :menu="menu" :column="column" :isStacked="isStacked" :refresher="() => timeline.reloadTimeline()">
<template #header>
<i class="ti ti-list"></i><span style="margin-left: 8px;">{{ column.name }}</span>
</template>

View File

@ -4,10 +4,10 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<XColumn :column="column" :isStacked="isStacked">
<XColumn :column="column" :isStacked="isStacked" :refresher="() => reloadTimeline()">
<template #header><i class="ti ti-at" style="margin-right: 8px;"></i>{{ column.name }}</template>
<MkNotes :pagination="pagination"/>
<MkNotes ref="tlComponent" :pagination="pagination"/>
</XColumn>
</template>
@ -22,6 +22,16 @@ defineProps<{
isStacked: boolean;
}>();
const tlComponent: InstanceType<typeof MkNotes> = $ref();
function reloadTimeline() {
return new Promise<void>((res) => {
tlComponent.pagingComponent?.reload().then(() => {
res();
});
});
}
const pagination = {
endpoint: 'notes/mentions' as const,
limit: 10,

View File

@ -4,10 +4,10 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<XColumn :column="column" :isStacked="isStacked" :menu="menu">
<XColumn :column="column" :isStacked="isStacked" :menu="menu" :refresher="() => notificationsComponent.reload()">
<template #header><i class="ti ti-bell" style="margin-right: 8px;"></i>{{ column.name }}</template>
<XNotifications :excludeTypes="props.column.excludeTypes"/>
<XNotifications ref="notificationsComponent" :excludeTypes="props.column.excludeTypes"/>
</XColumn>
</template>
@ -24,6 +24,8 @@ const props = defineProps<{
isStacked: boolean;
}>();
let notificationsComponent = $shallowRef<InstanceType<typeof XNotifications>>();
function func() {
os.popup(defineAsyncComponent(() => import('@/components/MkNotificationSelectWindow.vue')), {
excludeTypes: props.column.excludeTypes,

View File

@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<XColumn :menu="menu" :column="column" :isStacked="isStacked">
<XColumn :menu="menu" :column="column" :isStacked="isStacked" :refresher="() => timeline.reloadTimeline()">
<template #header>
<i class="ti ti-badge"></i><span style="margin-left: 8px;">{{ column.name }}</span>
</template>

View File

@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<XColumn :menu="menu" :column="column" :isStacked="isStacked">
<XColumn :menu="menu" :column="column" :isStacked="isStacked" :refresher="() => timeline.reloadTimeline()">
<template #header>
<i v-if="column.tl === 'home'" class="ti ti-home"></i>
<i v-else-if="column.tl === 'local'" class="ti ti-planet"></i>
@ -48,6 +48,7 @@ const props = defineProps<{
}>();
let disabled = $ref(false);
let timeline = $shallowRef<InstanceType<typeof MkTimeline>>();
const isLocalTimelineAvailable = (($i == null && instance.policies.ltlAvailable) || ($i != null && $i.policies.ltlAvailable));
const isGlobalTimelineAvailable = (($i == null && instance.policies.gtlAvailable) || ($i != null && $i.policies.gtlAvailable));

View File

@ -1061,7 +1061,7 @@
"💰": ["dollar", "payment", "coins", "sale"],
"🪙": ["dollar", "payment", "coins", "sale"],
"💳": ["money", "sales", "dollar", "bill", "payment", "shopping"],
"🪫": [],
"🪪": [],
"💎": ["blue", "ruby", "diamond", "jewelry"],
"⚖": ["law", "fairness", "weight"],
"🧰": ["tools", "diy", "fix", "maintainer", "mechanic"],

View File

@ -674,8 +674,8 @@ importers:
specifier: 3.3.7
version: 3.3.7
aiscript-vscode:
specifier: github:aiscript-dev/aiscript-vscode#v0.0.5
version: github.com/aiscript-dev/aiscript-vscode/a8fa5bb41885391cdb6a6e3165eaa6e4868da86e
specifier: github:aiscript-dev/aiscript-vscode#v0.0.6
version: github.com/aiscript-dev/aiscript-vscode/b5a8aa0ad927831a0b867d1c183460a14e6c48cd
astring:
specifier: 1.8.6
version: 1.8.6
@ -6931,7 +6931,7 @@ packages:
hasBin: true
peerDependencies:
'@swc/core': ^1.2.66
chokidar: 3.5.3
chokidar: ^3.5.1
peerDependenciesMeta:
chokidar:
optional: true
@ -19784,10 +19784,10 @@ packages:
readable-stream: 3.6.0
dev: false
github.com/aiscript-dev/aiscript-vscode/a8fa5bb41885391cdb6a6e3165eaa6e4868da86e:
resolution: {tarball: https://codeload.github.com/aiscript-dev/aiscript-vscode/tar.gz/a8fa5bb41885391cdb6a6e3165eaa6e4868da86e}
github.com/aiscript-dev/aiscript-vscode/b5a8aa0ad927831a0b867d1c183460a14e6c48cd:
resolution: {tarball: https://codeload.github.com/aiscript-dev/aiscript-vscode/tar.gz/b5a8aa0ad927831a0b867d1c183460a14e6c48cd}
name: aiscript-vscode
version: 0.0.5
version: 0.0.6
engines: {vscode: ^1.83.0}
dev: false