0
0

Merge remote-tracking branch 'upstream/develop' into develop
Some checks failed
Check SPDX-License-Identifier / check-spdx-license-id (push) Has been cancelled
Check copyright year / check_copyright_year (push) Has been cancelled
Publish Docker image (develop) / Build (linux/amd64) (push) Has been cancelled
Publish Docker image (develop) / Build (linux/arm64) (push) Has been cancelled
Publish Docker image (develop) / merge (push) Has been cancelled
Dockle / dockle (push) Has been cancelled
Storybook / build (push) Has been cancelled
Test (production install and build) / production (20.16.0) (push) Has been cancelled

This commit is contained in:
オスカー、 2024-10-21 12:03:03 +00:00
commit d13d354707
58 changed files with 521 additions and 263 deletions

View File

@ -1,3 +1,22 @@
## Unreleased
### General
- Feat: コンテンツの表示にログインを必須にできるように
### Client
- Enhance: Bull DashboardでRelationship Queueの状態も確認できるように
(Cherry-picked from https://github.com/MisskeyIO/misskey/pull/751)
- Enhance: ドライブでソートができるように
- Enhance: 投稿フォームでEscキーを押したときIME入力中ならフォームを閉じないように #10866
- Fix: 通知の範囲指定の設定項目が必要ない通知設定でも範囲指定の設定がでている問題を修正
- Fix: Turnstileが失敗・期限切れした際にも成功扱いとなってしまう問題を修正
(Cherry-picked from https://github.com/MisskeyIO/misskey/pull/768)
- Fix: デッキのタイムラインカラムで「センシティブなファイルを含むノートを表示」設定が使用できなかった問題を修正
### Server
-
## 2024.10.1 ## 2024.10.1
### Note ### Note

View File

@ -64,6 +64,22 @@ Thank you for your PR! Before creating a PR, please check the following:
Thanks for your cooperation 🤗 Thanks for your cooperation 🤗
### Additional things for ActivityPub payload changes
*This section is specific to misskey-dev implementation. Other fork or implementation may take different way. A significant difference is that non-"misskey-dev" extension is not described in the misskey-hub's document.*
If PR includes changes to ActivityPub payload, please reflect it in [misskey-hub's document](https://github.com/misskey-dev/misskey-hub-next/blob/master/content/ns.md) by sending PR.
The name of purporsed extension property (referred as "extended property" in later) to ActivityPub shall be prefixed by `_misskey_`. (i.e. `_misskey_quote`)
The extended property in `packages/backend/src/core/activitypub/type.ts` **must** be declared as optional because ActivityPub payloads that comes from older Misskey or other implementation may not contain it.
The extended property must be included in the context definition. Context is defined in `packages/backend/src/core/activitypub/misc/contexts.ts`.
The key shall be same as the name of extended property, and the value shall be same as "short IRI".
"Short IRI" is defined in misskey-hub's document, but usually takes form of `misskey:<name of extended property>`. (i.e. `misskey:_misskey_quote`)
One should not add property that has defined before by other implementation, or add custom variant value to "well-known" property.
## Reviewers guide ## Reviewers guide
Be willing to comment on the good points and not just the things you want fixed 💯 Be willing to comment on the good points and not just the things you want fixed 💯

32
locales/index.d.ts vendored
View File

@ -5194,6 +5194,32 @@ export interface Locale extends ILocale {
* 使 * 使
*/ */
"yourNameContainsProhibitedWordsDescription": string; "yourNameContainsProhibitedWordsDescription": string;
/**
* 稿
*/
"thisContentsAreMarkedAsSigninRequiredByAuthor": string;
/**
*
*/
"lockdown": string;
"_accountSettings": {
/**
*
*/
"requireSigninToViewContents": string;
/**
*
*/
"requireSigninToViewContentsDescription1": string;
/**
* URLプレビュー(OGP)Webページへの埋め込み
*/
"requireSigninToViewContentsDescription2": string;
/**
*
*/
"requireSigninToViewContentsDescription3": string;
};
"_abuseUserReport": { "_abuseUserReport": {
/** /**
* *
@ -9279,7 +9305,7 @@ export interface Locale extends ILocale {
*/ */
"youGotQuote": ParameterizedString<"name">; "youGotQuote": ParameterizedString<"name">;
/** /**
* {name}Renoteしまし * {name}
*/ */
"youRenoted": ParameterizedString<"name">; "youRenoted": ParameterizedString<"name">;
/** /**
@ -9384,7 +9410,7 @@ export interface Locale extends ILocale {
*/ */
"reply": string; "reply": string;
/** /**
* Renote *
*/ */
"renote": string; "renote": string;
/** /**
@ -9442,7 +9468,7 @@ export interface Locale extends ILocale {
*/ */
"reply": string; "reply": string;
/** /**
* Renote *
*/ */
"renote": string; "renote": string;
}; };

View File

@ -1294,6 +1294,14 @@ prohibitedWordsForNameOfUser: "禁止ワード(ユーザーの名前)"
prohibitedWordsForNameOfUserDescription: "このリストに含まれる文字列がユーザーの名前に含まれる場合、ユーザーの名前の変更を拒否します。モデレーター権限を持つユーザーはこの制限の影響を受けません。" prohibitedWordsForNameOfUserDescription: "このリストに含まれる文字列がユーザーの名前に含まれる場合、ユーザーの名前の変更を拒否します。モデレーター権限を持つユーザーはこの制限の影響を受けません。"
yourNameContainsProhibitedWords: "変更しようとした名前に禁止された文字列が含まれています" yourNameContainsProhibitedWords: "変更しようとした名前に禁止された文字列が含まれています"
yourNameContainsProhibitedWordsDescription: "名前に禁止されている文字列が含まれています。この名前を使用したい場合は、サーバー管理者にお問い合わせください。" yourNameContainsProhibitedWordsDescription: "名前に禁止されている文字列が含まれています。この名前を使用したい場合は、サーバー管理者にお問い合わせください。"
thisContentsAreMarkedAsSigninRequiredByAuthor: "投稿者により、表示にはログインが必要と設定されています"
lockdown: "ロックダウン"
_accountSettings:
requireSigninToViewContents: "コンテンツの表示にログインを必須にする"
requireSigninToViewContentsDescription1: "あなたが作成した全てのノートなどのコンテンツを表示するのにログインを必須にします。クローラーから情報を収集されるのを防ぐ効果が期待できます。"
requireSigninToViewContentsDescription2: "URLプレビュー(OGP)、Webページへの埋め込み、ートの引用に対応していないサーバーからの表示も不可になります。"
requireSigninToViewContentsDescription3: "リモートサーバーに連合されたコンテンツでは、これらの制限が適用されない場合があります。"
_abuseUserReport: _abuseUserReport:
forward: "転送" forward: "転送"
@ -2450,7 +2458,7 @@ _notification:
youGotMention: "{name}からのメンション" youGotMention: "{name}からのメンション"
youGotReply: "{name}からのリプライ" youGotReply: "{name}からのリプライ"
youGotQuote: "{name}による引用" youGotQuote: "{name}による引用"
youRenoted: "{name}がRenoteしました" youRenoted: "{name}がリノートしました"
youWereFollowed: "フォローされました" youWereFollowed: "フォローされました"
youReceivedFollowRequest: "フォローリクエストが来ました" youReceivedFollowRequest: "フォローリクエストが来ました"
yourFollowRequestAccepted: "フォローリクエストが承認されました" yourFollowRequestAccepted: "フォローリクエストが承認されました"
@ -2478,7 +2486,7 @@ _notification:
follow: "フォロー" follow: "フォロー"
mention: "メンション" mention: "メンション"
reply: "リプライ" reply: "リプライ"
renote: "Renote" renote: "リノート"
quote: "引用" quote: "引用"
reaction: "リアクション" reaction: "リアクション"
pollEnded: "アンケートが終了" pollEnded: "アンケートが終了"
@ -2494,7 +2502,7 @@ _notification:
_actions: _actions:
followBack: "フォローバック" followBack: "フォローバック"
reply: "返信" reply: "返信"
renote: "Renote" renote: "リノート"
_deck: _deck:
alwaysShowMainColumn: "常にメインカラムを表示" alwaysShowMainColumn: "常にメインカラムを表示"

View File

@ -0,0 +1,16 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class SigninRequiredForShowContents1729333924409 {
name = 'SigninRequiredForShowContents1729333924409'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "user" ADD "requireSigninToViewContents" boolean NOT NULL DEFAULT false`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "requireSigninToViewContents"`);
}
}

View File

@ -83,6 +83,7 @@ function generateDummyUser(override?: Partial<MiUser>): MiUser {
isExplorable: true, isExplorable: true,
isHibernated: false, isHibernated: false,
isDeleted: false, isDeleted: false,
requireSigninToViewContents: false,
emojis: [], emojis: [],
score: 0, score: 0,
host: null, host: null,

View File

@ -497,6 +497,7 @@ export class ApRendererService {
summary: profile.description ? this.mfmService.toHtml(mfm.parse(profile.description)) : null, summary: profile.description ? this.mfmService.toHtml(mfm.parse(profile.description)) : null,
_misskey_summary: profile.description, _misskey_summary: profile.description,
_misskey_followedMessage: profile.followedMessage, _misskey_followedMessage: profile.followedMessage,
_misskey_requireSigninToViewContents: user.requireSigninToViewContents,
icon: avatar ? this.renderImage(avatar) : null, icon: avatar ? this.renderImage(avatar) : null,
image: banner ? this.renderImage(banner) : null, image: banner ? this.renderImage(banner) : null,
tag, tag,

View File

@ -555,6 +555,7 @@ const extension_context_definition = {
'_misskey_votes': 'misskey:_misskey_votes', '_misskey_votes': 'misskey:_misskey_votes',
'_misskey_summary': 'misskey:_misskey_summary', '_misskey_summary': 'misskey:_misskey_summary',
'_misskey_followedMessage': 'misskey:_misskey_followedMessage', '_misskey_followedMessage': 'misskey:_misskey_followedMessage',
'_misskey_requireSigninToViewContents': 'misskey:_misskey_requireSigninToViewContents',
'isCat': 'misskey:isCat', 'isCat': 'misskey:isCat',
// vcard // vcard
vcard: 'http://www.w3.org/2006/vcard/ns#', vcard: 'http://www.w3.org/2006/vcard/ns#',

View File

@ -356,6 +356,7 @@ export class ApPersonService implements OnModuleInit {
tags, tags,
isBot, isBot,
isCat: (person as any).isCat === true, isCat: (person as any).isCat === true,
requireSigninToViewContents: (person as any).requireSigninToViewContents === true,
emojis, emojis,
})) as MiRemoteUser; })) as MiRemoteUser;

View File

@ -14,6 +14,7 @@ export interface IObject {
summary?: string; summary?: string;
_misskey_summary?: string; _misskey_summary?: string;
_misskey_followedMessage?: string | null; _misskey_followedMessage?: string | null;
_misskey_requireSigninToViewContents?: boolean;
published?: string; published?: string;
updated?: string; updated?: string;
cc?: ApObject; cc?: ApObject;

View File

@ -149,6 +149,10 @@ export class NoteEntityService implements OnModuleInit {
} }
} }
if (packedNote.user.requireSigninToViewContents && meId == null) {
hide = true;
}
if (hide) { if (hide) {
packedNote.visibleUserIds = undefined; packedNote.visibleUserIds = undefined;
packedNote.fileIds = []; packedNote.fileIds = [];

View File

@ -490,6 +490,7 @@ export class UserEntityService implements OnModuleInit {
}))) : [], }))) : [],
isBot: user.isBot, isBot: user.isBot,
isCat: user.isCat, isCat: user.isCat,
requireSigninToViewContents: user.requireSigninToViewContents === false ? undefined : true,
instance: user.host ? this.federatedInstanceService.federatedInstanceCache.fetch(user.host).then(instance => instance ? { instance: user.host ? this.federatedInstanceService.federatedInstanceCache.fetch(user.host).then(instance => instance ? {
name: instance.name, name: instance.name,
softwareName: instance.softwareName, softwareName: instance.softwareName,

View File

@ -202,6 +202,11 @@ export class MiUser {
}) })
public isHibernated: boolean; public isHibernated: boolean;
@Column('boolean', {
default: false,
})
public requireSigninToViewContents: boolean;
// アカウントが削除されたかどうかのフラグだが、完全に削除される際は物理削除なので実質削除されるまでの「削除が進行しているかどうか」のフラグ // アカウントが削除されたかどうかのフラグだが、完全に削除される際は物理削除なので実質削除されるまでの「削除が進行しているかどうか」のフラグ
@Column('boolean', { @Column('boolean', {
default: false, default: false,

View File

@ -115,6 +115,10 @@ export const packedUserLiteSchema = {
type: 'boolean', type: 'boolean',
nullable: false, optional: true, nullable: false, optional: true,
}, },
requireSigninToViewContents: {
type: 'boolean',
nullable: false, optional: true,
},
instance: { instance: {
type: 'object', type: 'object',
nullable: false, optional: true, nullable: false, optional: true,

View File

@ -39,6 +39,17 @@ export class GetterService {
return note; return note;
} }
@bindThis
public async getNoteWithUser(noteId: MiNote['id']) {
const note = await this.notesRepository.findOne({ where: { id: noteId }, relations: ['user'] });
if (note == null) {
throw new IdentifiableError('9725d0ce-ba28-4dde-95a7-2cbb2c15de24', 'No such note.');
}
return note;
}
/** /**
* Get user for API processing * Get user for API processing
*/ */

View File

@ -179,6 +179,7 @@ export const paramDef = {
autoAcceptFollowed: { type: 'boolean' }, autoAcceptFollowed: { type: 'boolean' },
noCrawle: { type: 'boolean' }, noCrawle: { type: 'boolean' },
preventAiLearning: { type: 'boolean' }, preventAiLearning: { type: 'boolean' },
requireSigninToViewContents: { type: 'boolean' },
isBot: { type: 'boolean' }, isBot: { type: 'boolean' },
isCat: { type: 'boolean' }, isCat: { type: 'boolean' },
injectFeaturedNote: { type: 'boolean' }, injectFeaturedNote: { type: 'boolean' },
@ -334,6 +335,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (typeof ps.autoAcceptFollowed === 'boolean') profileUpdates.autoAcceptFollowed = ps.autoAcceptFollowed; if (typeof ps.autoAcceptFollowed === 'boolean') profileUpdates.autoAcceptFollowed = ps.autoAcceptFollowed;
if (typeof ps.noCrawle === 'boolean') profileUpdates.noCrawle = ps.noCrawle; if (typeof ps.noCrawle === 'boolean') profileUpdates.noCrawle = ps.noCrawle;
if (typeof ps.preventAiLearning === 'boolean') profileUpdates.preventAiLearning = ps.preventAiLearning; if (typeof ps.preventAiLearning === 'boolean') profileUpdates.preventAiLearning = ps.preventAiLearning;
if (typeof ps.requireSigninToViewContents === 'boolean') updates.requireSigninToViewContents = ps.requireSigninToViewContents;
if (typeof ps.isCat === 'boolean') updates.isCat = ps.isCat; if (typeof ps.isCat === 'boolean') updates.isCat = ps.isCat;
if (typeof ps.injectFeaturedNote === 'boolean') profileUpdates.injectFeaturedNote = ps.injectFeaturedNote; if (typeof ps.injectFeaturedNote === 'boolean') profileUpdates.injectFeaturedNote = ps.injectFeaturedNote;
if (typeof ps.receiveAnnouncementEmail === 'boolean') profileUpdates.receiveAnnouncementEmail = ps.receiveAnnouncementEmail; if (typeof ps.receiveAnnouncementEmail === 'boolean') profileUpdates.receiveAnnouncementEmail = ps.receiveAnnouncementEmail;

View File

@ -26,6 +26,12 @@ export const meta = {
code: 'NO_SUCH_NOTE', code: 'NO_SUCH_NOTE',
id: '24fcbfc6-2e37-42b6-8388-c29b3861a08d', id: '24fcbfc6-2e37-42b6-8388-c29b3861a08d',
}, },
signinRequired: {
message: 'Signin required.',
code: 'SIGNIN_REQUIRED',
id: '8e75455b-738c-471d-9f80-62693f33372e',
},
}, },
} as const; } as const;
@ -44,11 +50,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private getterService: GetterService, private getterService: GetterService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const note = await this.getterService.getNote(ps.noteId).catch(err => { const note = await this.getterService.getNoteWithUser(ps.noteId).catch(err => {
if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
throw err; throw err;
}); });
if (note.user!.requireSigninToViewContents && me == null) {
throw new ApiError(meta.errors.signinRequired);
}
return await this.noteEntityService.pack(note, me, { return await this.noteEntityService.pack(note, me, {
detail: true, detail: true,
}); });

View File

@ -42,6 +42,12 @@ export const meta = {
code: 'BOTH_WITH_REPLIES_AND_WITH_FILES', code: 'BOTH_WITH_REPLIES_AND_WITH_FILES',
id: '91c8cb9f-36ed-46e7-9ca2-7df96ed6e222', id: '91c8cb9f-36ed-46e7-9ca2-7df96ed6e222',
}, },
signinRequired: {
message: 'Signin required.',
code: 'SIGNIN_REQUIRED',
id: 'd1588a9e-4b4d-4c07-807f-16f1486577a2',
},
}, },
} as const; } as const;

View File

@ -30,6 +30,7 @@ import type {
EndedPollNotificationQueue, EndedPollNotificationQueue,
InboxQueue, InboxQueue,
ObjectStorageQueue, ObjectStorageQueue,
RelationshipQueue,
SystemQueue, SystemQueue,
UserWebhookDeliverQueue, UserWebhookDeliverQueue,
SystemWebhookDeliverQueue, SystemWebhookDeliverQueue,
@ -121,6 +122,7 @@ export class ClientServerService {
@Inject('queue:deliver') public deliverQueue: DeliverQueue, @Inject('queue:deliver') public deliverQueue: DeliverQueue,
@Inject('queue:inbox') public inboxQueue: InboxQueue, @Inject('queue:inbox') public inboxQueue: InboxQueue,
@Inject('queue:db') public dbQueue: DbQueue, @Inject('queue:db') public dbQueue: DbQueue,
@Inject('queue:relationship') public relationshipQueue: RelationshipQueue,
@Inject('queue:objectStorage') public objectStorageQueue: ObjectStorageQueue, @Inject('queue:objectStorage') public objectStorageQueue: ObjectStorageQueue,
@Inject('queue:userWebhookDeliver') public userWebhookDeliverQueue: UserWebhookDeliverQueue, @Inject('queue:userWebhookDeliver') public userWebhookDeliverQueue: UserWebhookDeliverQueue,
@Inject('queue:systemWebhookDeliver') public systemWebhookDeliverQueue: SystemWebhookDeliverQueue, @Inject('queue:systemWebhookDeliver') public systemWebhookDeliverQueue: SystemWebhookDeliverQueue,
@ -255,6 +257,7 @@ export class ClientServerService {
this.deliverQueue, this.deliverQueue,
this.inboxQueue, this.inboxQueue,
this.dbQueue, this.dbQueue,
this.relationshipQueue,
this.objectStorageQueue, this.objectStorageQueue,
this.userWebhookDeliverQueue, this.userWebhookDeliverQueue,
this.systemWebhookDeliverQueue, this.systemWebhookDeliverQueue,
@ -605,12 +608,15 @@ export class ClientServerService {
fastify.get<{ Params: { note: string; } }>('/notes/:note', async (request, reply) => { fastify.get<{ Params: { note: string; } }>('/notes/:note', async (request, reply) => {
vary(reply.raw, 'Accept'); vary(reply.raw, 'Accept');
const note = await this.notesRepository.findOneBy({ const note = await this.notesRepository.findOne({
where: {
id: request.params.note, id: request.params.note,
visibility: In(['public', 'home']), visibility: In(['public', 'home']),
},
relations: ['user'],
}); });
if (note) { if (note && !note.user!.requireSigninToViewContents) {
const _note = await this.noteEntityService.pack(note); const _note = await this.noteEntityService.pack(note);
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: note.userId }); const profile = await this.userProfilesRepository.findOneByOrFail({ userId: note.userId });
reply.header('Cache-Control', 'public, max-age=15'); reply.header('Cache-Control', 'public, max-age=15');

View File

@ -5,12 +5,12 @@
import { defineAsyncComponent, reactive, ref } from 'vue'; import { defineAsyncComponent, reactive, ref } from 'vue';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import { apiUrl } from '@@/js/config.js';
import type { MenuItem, MenuButton } from '@/types/menu.js';
import { showSuspendedDialog } from '@/scripts/show-suspended-dialog.js'; import { showSuspendedDialog } from '@/scripts/show-suspended-dialog.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { miLocalStorage } from '@/local-storage.js'; import { miLocalStorage } from '@/local-storage.js';
import type { MenuItem, MenuButton } from '@/types/menu.js';
import { del, get, set } from '@/scripts/idb-proxy.js'; import { del, get, set } from '@/scripts/idb-proxy.js';
import { apiUrl } from '@@/js/config.js';
import { waiting, popup, popupMenu, success, alert } from '@/os.js'; import { waiting, popup, popupMenu, success, alert } from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js'; import { misskeyApi } from '@/scripts/misskey-api.js';
import { unisonReload, reloadChannel } from '@/scripts/unison-reload.js'; import { unisonReload, reloadChannel } from '@/scripts/unison-reload.js';
@ -165,7 +165,18 @@ function fetchAccount(token: string, id?: string, forceShowDialog?: boolean): Pr
}); });
} }
export function updateAccount(accountData: Partial<Account>) { export function updateAccount(accountData: Account) {
if (!$i) return;
for (const key of Object.keys($i)) {
delete $i[key];
}
for (const [key, value] of Object.entries(accountData)) {
$i[key] = value;
}
miLocalStorage.setItem('account', JSON.stringify($i));
}
export function updateAccountPartial(accountData: Partial<Account>) {
if (!$i) return; if (!$i) return;
for (const [key, value] of Object.entries(accountData)) { for (const [key, value] of Object.entries(accountData)) {
$i[key] = value; $i[key] = value;

View File

@ -4,14 +4,14 @@
*/ */
import { createApp, defineAsyncComponent, markRaw } from 'vue'; import { createApp, defineAsyncComponent, markRaw } from 'vue';
import { ui } from '@@/js/config.js';
import { common } from './common.js'; import { common } from './common.js';
import type * as Misskey from 'misskey-js'; import type * as Misskey from 'misskey-js';
import { ui } from '@@/js/config.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { alert, confirm, popup, post, toast } from '@/os.js'; import { alert, confirm, popup, post, toast } from '@/os.js';
import { useStream } from '@/stream.js'; import { useStream } from '@/stream.js';
import * as sound from '@/scripts/sound.js'; import * as sound from '@/scripts/sound.js';
import { $i, signout, updateAccount } from '@/account.js'; import { $i, signout, updateAccountPartial } from '@/account.js';
import { instance } from '@/instance.js'; import { instance } from '@/instance.js';
import { ColdDeviceStorage, defaultStore } from '@/store.js'; import { ColdDeviceStorage, defaultStore } from '@/store.js';
import { reactionPicker } from '@/scripts/reaction-picker.js'; import { reactionPicker } from '@/scripts/reaction-picker.js';
@ -291,11 +291,11 @@ export async function mainBoot() {
// 自分の情報が更新されたとき // 自分の情報が更新されたとき
main.on('meUpdated', i => { main.on('meUpdated', i => {
updateAccount(i); updateAccountPartial(i);
}); });
main.on('readAllNotifications', () => { main.on('readAllNotifications', () => {
updateAccount({ updateAccountPartial({
hasUnreadNotification: false, hasUnreadNotification: false,
unreadNotificationsCount: 0, unreadNotificationsCount: 0,
}); });
@ -303,39 +303,39 @@ export async function mainBoot() {
main.on('unreadNotification', () => { main.on('unreadNotification', () => {
const unreadNotificationsCount = ($i?.unreadNotificationsCount ?? 0) + 1; const unreadNotificationsCount = ($i?.unreadNotificationsCount ?? 0) + 1;
updateAccount({ updateAccountPartial({
hasUnreadNotification: true, hasUnreadNotification: true,
unreadNotificationsCount, unreadNotificationsCount,
}); });
}); });
main.on('unreadMention', () => { main.on('unreadMention', () => {
updateAccount({ hasUnreadMentions: true }); updateAccountPartial({ hasUnreadMentions: true });
}); });
main.on('readAllUnreadMentions', () => { main.on('readAllUnreadMentions', () => {
updateAccount({ hasUnreadMentions: false }); updateAccountPartial({ hasUnreadMentions: false });
}); });
main.on('unreadSpecifiedNote', () => { main.on('unreadSpecifiedNote', () => {
updateAccount({ hasUnreadSpecifiedNotes: true }); updateAccountPartial({ hasUnreadSpecifiedNotes: true });
}); });
main.on('readAllUnreadSpecifiedNotes', () => { main.on('readAllUnreadSpecifiedNotes', () => {
updateAccount({ hasUnreadSpecifiedNotes: false }); updateAccountPartial({ hasUnreadSpecifiedNotes: false });
}); });
main.on('readAllAntennas', () => { main.on('readAllAntennas', () => {
updateAccount({ hasUnreadAntenna: false }); updateAccountPartial({ hasUnreadAntenna: false });
}); });
main.on('unreadAntenna', () => { main.on('unreadAntenna', () => {
updateAccount({ hasUnreadAntenna: true }); updateAccountPartial({ hasUnreadAntenna: true });
sound.playMisskeySfx('antenna'); sound.playMisskeySfx('antenna');
}); });
main.on('readAllAnnouncements', () => { main.on('readAllAnnouncements', () => {
updateAccount({ hasUnreadAnnouncement: false }); updateAccountPartial({ hasUnreadAnnouncement: false });
}); });
// 個人宛てお知らせが発行されたとき // 個人宛てお知らせが発行されたとき

View File

@ -29,7 +29,7 @@ import { misskeyApi } from '@/scripts/misskey-api.js';
import MkModal from '@/components/MkModal.vue'; import MkModal from '@/components/MkModal.vue';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { $i, updateAccount } from '@/account.js'; import { $i, updateAccountPartial } from '@/account.js';
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
announcement: Misskey.entities.Announcement; announcement: Misskey.entities.Announcement;
@ -51,7 +51,7 @@ async function ok() {
modal.value?.close(); modal.value?.close();
misskeyApi('i/read-announcement', { announcementId: props.announcement.id }); misskeyApi('i/read-announcement', { announcementId: props.announcement.id });
updateAccount({ updateAccountPartial({
unreadAnnouncements: $i!.unreadAnnouncements.filter(a => a.id !== props.announcement.id), unreadAnnouncements: $i!.unreadAnnouncements.filter(a => a.id !== props.announcement.id),
}); });
} }

View File

@ -117,8 +117,8 @@ async function requestRender() {
sitekey: props.sitekey, sitekey: props.sitekey,
theme: defaultStore.state.darkMode ? 'dark' : 'light', theme: defaultStore.state.darkMode ? 'dark' : 'light',
callback: callback, callback: callback,
'expired-callback': callback, 'expired-callback': () => callback(undefined),
'error-callback': callback, 'error-callback': () => callback(undefined),
}); });
} else if (props.provider === 'mcaptcha' && props.instanceUrl && props.sitekey) { } else if (props.provider === 'mcaptcha' && props.instanceUrl && props.sitekey) {
const { default: Widget } = await import('@mcaptcha/vanilla-glue'); const { default: Widget } = await import('@mcaptcha/vanilla-glue');

View File

@ -64,26 +64,30 @@ const showBody = ref(props.expanded);
const ignoreOmit = ref(false); const ignoreOmit = ref(false);
const omitted = ref(false); const omitted = ref(false);
function enter(el) { function enter(el: Element) {
if (!(el instanceof HTMLElement)) return;
const elementHeight = el.getBoundingClientRect().height; const elementHeight = el.getBoundingClientRect().height;
el.style.height = 0; el.style.height = '0';
el.offsetHeight; // reflow el.offsetHeight; // reflow
el.style.height = Math.min(elementHeight, props.maxHeight ?? Infinity) + 'px'; el.style.height = `${Math.min(elementHeight, props.maxHeight ?? Infinity)}px`;
} }
function afterEnter(el) { function afterEnter(el: Element) {
el.style.height = null; if (!(el instanceof HTMLElement)) return;
el.style.height = '';
} }
function leave(el) { function leave(el: Element) {
if (!(el instanceof HTMLElement)) return;
const elementHeight = el.getBoundingClientRect().height; const elementHeight = el.getBoundingClientRect().height;
el.style.height = elementHeight + 'px'; el.style.height = `${elementHeight}px`;
el.offsetHeight; // reflow el.offsetHeight; // reflow
el.style.height = 0; el.style.height = '0';
} }
function afterLeave(el) { function afterLeave(el: Element) {
el.style.height = null; if (!(el instanceof HTMLElement)) return;
el.style.height = '';
} }
const calcOmit = () => { const calcOmit = () => {

View File

@ -128,14 +128,14 @@ export default defineComponent({
return children; return children;
}; };
function onBeforeLeave(element: Element) { function onBeforeLeave(el: Element) {
const el = element as HTMLElement; if (!(el instanceof HTMLElement)) return;
el.style.top = `${el.offsetTop}px`; el.style.top = `${el.offsetTop}px`;
el.style.left = `${el.offsetLeft}px`; el.style.left = `${el.offsetLeft}px`;
} }
function onLeaveCancelled(element: Element) { function onLeaveCancelled(el: Element) {
const el = element as HTMLElement; if (!(el instanceof HTMLElement)) return;
el.style.top = ''; el.style.top = '';
el.style.left = ''; el.style.left = '';
} }

View File

@ -157,7 +157,12 @@ const ilFilesObserver = new IntersectionObserver(
(entries) => entries.some((entry) => entry.isIntersecting) && !fetching.value && moreFiles.value && fetchMoreFiles(), (entries) => entries.some((entry) => entry.isIntersecting) && !fetching.value && moreFiles.value && fetchMoreFiles(),
); );
const sortModeSelect = ref('+createdAt');
watch(folder, () => emit('cd', folder.value)); watch(folder, () => emit('cd', folder.value));
watch(sortModeSelect, () => {
fetch();
});
function onStreamDriveFileCreated(file: Misskey.entities.DriveFile) { function onStreamDriveFileCreated(file: Misskey.entities.DriveFile) {
addFile(file, true); addFile(file, true);
@ -558,6 +563,7 @@ async function fetch() {
folderId: folder.value ? folder.value.id : null, folderId: folder.value ? folder.value.id : null,
type: props.type, type: props.type,
limit: filesMax + 1, limit: filesMax + 1,
sort: sortModeSelect.value,
}).then(fetchedFiles => { }).then(fetchedFiles => {
if (fetchedFiles.length === filesMax + 1) { if (fetchedFiles.length === filesMax + 1) {
moreFiles.value = true; moreFiles.value = true;
@ -607,6 +613,7 @@ function fetchMoreFiles() {
type: props.type, type: props.type,
untilId: files.value.at(-1)?.id, untilId: files.value.at(-1)?.id,
limit: max + 1, limit: max + 1,
sort: sortModeSelect.value,
}).then(files => { }).then(files => {
if (files.length === max + 1) { if (files.length === max + 1) {
moreFiles.value = true; moreFiles.value = true;
@ -642,6 +649,43 @@ function getMenu() {
type: 'label', type: 'label',
}); });
menu.push({
type: 'parent',
text: i18n.ts.sort,
icon: 'ti ti-arrows-sort',
children: [{
text: `${i18n.ts.registeredDate} (${i18n.ts.descendingOrder})`,
icon: 'ti ti-sort-descending-letters',
action: () => { sortModeSelect.value = '+createdAt'; },
active: sortModeSelect.value === '+createdAt',
}, {
text: `${i18n.ts.registeredDate} (${i18n.ts.ascendingOrder})`,
icon: 'ti ti-sort-ascending-letters',
action: () => { sortModeSelect.value = '-createdAt'; },
active: sortModeSelect.value === '-createdAt',
}, {
text: `${i18n.ts.size} (${i18n.ts.descendingOrder})`,
icon: 'ti ti-sort-descending-letters',
action: () => { sortModeSelect.value = '+size'; },
active: sortModeSelect.value === '+size',
}, {
text: `${i18n.ts.size} (${i18n.ts.ascendingOrder})`,
icon: 'ti ti-sort-ascending-letters',
action: () => { sortModeSelect.value = '-size'; },
active: sortModeSelect.value === '-size',
}, {
text: `${i18n.ts.name} (${i18n.ts.descendingOrder})`,
icon: 'ti ti-sort-descending-letters',
action: () => { sortModeSelect.value = '+name'; },
active: sortModeSelect.value === '+name',
}, {
text: `${i18n.ts.name} (${i18n.ts.ascendingOrder})`,
icon: 'ti ti-sort-ascending-letters',
action: () => { sortModeSelect.value = '-name'; },
active: sortModeSelect.value === '-name',
}],
});
if (folder.value) { if (folder.value) {
menu.push({ menu.push({
text: i18n.ts.renameFolder, text: i18n.ts.renameFolder,

View File

@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template> <template>
<div ref="rootEl" :class="$style.root"> <div ref="rootEl" :class="$style.root">
<header :class="$style.header" class="_button" :style="{ background: bg }" @click="showBody = !showBody"> <header :class="$style.header" class="_button" @click="showBody = !showBody">
<div :class="$style.title"><div><slot name="header"></slot></div></div> <div :class="$style.title"><div><slot name="header"></slot></div></div>
<div :class="$style.divider"></div> <div :class="$style.divider"></div>
<button class="_button" :class="$style.button"> <button class="_button" :class="$style.button">
@ -32,21 +32,23 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted, ref, shallowRef, watch } from 'vue'; import { onMounted, ref, shallowRef, watch } from 'vue';
import tinycolor from 'tinycolor2';
import { miLocalStorage } from '@/local-storage.js'; import { miLocalStorage } from '@/local-storage.js';
import { defaultStore } from '@/store.js'; import { defaultStore } from '@/store.js';
import { getBgColor } from '@/scripts/get-bg-color.js';
const miLocalStoragePrefix = 'ui:folder:' as const; const miLocalStoragePrefix = 'ui:folder:' as const;
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
expanded?: boolean; expanded?: boolean;
persistKey?: string; persistKey?: string | null;
}>(), { }>(), {
expanded: true, expanded: true,
persistKey: null,
}); });
const rootEl = shallowRef<HTMLDivElement>(); const rootEl = shallowRef<HTMLElement>();
const bg = ref<string>(); const parentBg = ref<string | null>(null);
// eslint-disable-next-line vue/no-setup-props-reactivity-loss
const showBody = ref((props.persistKey && miLocalStorage.getItem(`${miLocalStoragePrefix}${props.persistKey}`)) ? (miLocalStorage.getItem(`${miLocalStoragePrefix}${props.persistKey}`) === 't') : props.expanded); const showBody = ref((props.persistKey && miLocalStorage.getItem(`${miLocalStoragePrefix}${props.persistKey}`)) ? (miLocalStorage.getItem(`${miLocalStoragePrefix}${props.persistKey}`) === 't') : props.expanded);
watch(showBody, () => { watch(showBody, () => {
@ -55,47 +57,34 @@ watch(showBody, () => {
} }
}); });
function enter(element: Element) { function enter(el: Element) {
const el = element as HTMLElement; if (!(el instanceof HTMLElement)) return;
const elementHeight = el.getBoundingClientRect().height; const elementHeight = el.getBoundingClientRect().height;
el.style.height = '0'; el.style.height = '0';
el.offsetHeight; // reflow el.offsetHeight; // reflow
el.style.height = elementHeight + 'px'; el.style.height = `${elementHeight}px`;
} }
function afterEnter(element: Element) { function afterEnter(el: Element) {
const el = element as HTMLElement; if (!(el instanceof HTMLElement)) return;
el.style.height = 'unset'; el.style.height = '';
} }
function leave(element: Element) { function leave(el: Element) {
const el = element as HTMLElement; if (!(el instanceof HTMLElement)) return;
const elementHeight = el.getBoundingClientRect().height; const elementHeight = el.getBoundingClientRect().height;
el.style.height = elementHeight + 'px'; el.style.height = `${elementHeight}px`;
el.offsetHeight; // reflow el.offsetHeight; // reflow
el.style.height = '0'; el.style.height = '0';
} }
function afterLeave(element: Element) { function afterLeave(el: Element) {
const el = element as HTMLElement; if (!(el instanceof HTMLElement)) return;
el.style.height = 'unset'; el.style.height = '';
} }
onMounted(() => { onMounted(() => {
function getParentBg(el?: HTMLElement | null): string { parentBg.value = getBgColor(rootEl.value?.parentElement);
if (el == null || el.tagName === 'BODY') return 'var(--MI_THEME-bg)';
const background = el.style.background || el.style.backgroundColor;
if (background) {
return background;
} else {
return getParentBg(el.parentElement);
}
}
const rawBg = getParentBg(rootEl.value);
const _bg = tinycolor(rawBg.startsWith('var(') ? getComputedStyle(document.documentElement).getPropertyValue(rawBg.slice(4, -1)) : rawBg);
_bg.setAlpha(0.85);
bg.value = _bg.toRgbString();
}); });
</script> </script>
@ -121,6 +110,7 @@ onMounted(() => {
top: var(--MI-stickyTop, 0px); top: var(--MI-stickyTop, 0px);
-webkit-backdrop-filter: var(--MI-blur, blur(8px)); -webkit-backdrop-filter: var(--MI-blur, blur(8px));
backdrop-filter: var(--MI-blur, blur(20px)); backdrop-filter: var(--MI-blur, blur(20px));
background-color: color(from v-bind("parentBg ?? 'var(--bg)'") srgb r g b / 0.85);
} }
.title { .title {

View File

@ -56,8 +56,9 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { nextTick, onMounted, shallowRef, ref } from 'vue'; import { nextTick, onMounted, ref, shallowRef } from 'vue';
import { defaultStore } from '@/store.js'; import { defaultStore } from '@/store.js';
import { getBgColor } from '@/scripts/get-bg-color.js';
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
defaultOpen?: boolean; defaultOpen?: boolean;
@ -69,40 +70,35 @@ const props = withDefaults(defineProps<{
withSpacer: true, withSpacer: true,
}); });
const getBgColor = (el: HTMLElement) => {
const style = window.getComputedStyle(el);
if (style.backgroundColor && !['rgba(0, 0, 0, 0)', 'rgba(0,0,0,0)', 'transparent'].includes(style.backgroundColor)) {
return style.backgroundColor;
} else {
return el.parentElement ? getBgColor(el.parentElement) : 'transparent';
}
};
const rootEl = shallowRef<HTMLElement>(); const rootEl = shallowRef<HTMLElement>();
const bgSame = ref(false); const bgSame = ref(false);
const opened = ref(props.defaultOpen); const opened = ref(props.defaultOpen);
const openedAtLeastOnce = ref(props.defaultOpen); const openedAtLeastOnce = ref(props.defaultOpen);
function enter(el) { function enter(el: Element) {
if (!(el instanceof HTMLElement)) return;
const elementHeight = el.getBoundingClientRect().height; const elementHeight = el.getBoundingClientRect().height;
el.style.height = 0; el.style.height = '0';
el.offsetHeight; // reflow el.offsetHeight; // reflow
el.style.height = Math.min(elementHeight, props.maxHeight ?? Infinity) + 'px'; el.style.height = `${Math.min(elementHeight, props.maxHeight ?? Infinity)}px`;
} }
function afterEnter(el) { function afterEnter(el: Element) {
el.style.height = null; if (!(el instanceof HTMLElement)) return;
el.style.height = '';
} }
function leave(el) { function leave(el: Element) {
if (!(el instanceof HTMLElement)) return;
const elementHeight = el.getBoundingClientRect().height; const elementHeight = el.getBoundingClientRect().height;
el.style.height = elementHeight + 'px'; el.style.height = `${elementHeight}px`;
el.offsetHeight; // reflow el.offsetHeight; // reflow
el.style.height = 0; el.style.height = '0';
} }
function afterLeave(el) { function afterLeave(el: Element) {
el.style.height = null; if (!(el instanceof HTMLElement)) return;
el.style.height = '';
} }
function toggle() { function toggle() {
@ -117,7 +113,7 @@ function toggle() {
onMounted(() => { onMounted(() => {
const computedStyle = getComputedStyle(document.documentElement); const computedStyle = getComputedStyle(document.documentElement);
const parentBg = getBgColor(rootEl.value!.parentElement!); const parentBg = getBgColor(rootEl.value?.parentElement) ?? 'transparent';
const myBg = computedStyle.getPropertyValue('--MI_THEME-panel'); const myBg = computedStyle.getPropertyValue('--MI_THEME-panel');
bgSame.value = parentBg === myBg; bgSame.value = parentBg === myBg;
}); });

View File

@ -37,13 +37,13 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup> <script lang="ts" setup>
import { onBeforeUnmount, onMounted, ref } from 'vue'; import { onBeforeUnmount, onMounted, ref } from 'vue';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import { host } from '@@/js/config.js';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js'; import { misskeyApi } from '@/scripts/misskey-api.js';
import { useStream } from '@/stream.js'; import { useStream } from '@/stream.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { claimAchievement } from '@/scripts/achievements.js'; import { claimAchievement } from '@/scripts/achievements.js';
import { pleaseLogin } from '@/scripts/please-login.js'; import { pleaseLogin } from '@/scripts/please-login.js';
import { host } from '@@/js/config.js';
import { $i } from '@/account.js'; import { $i } from '@/account.js';
import { defaultStore } from '@/store.js'; import { defaultStore } from '@/store.js';
@ -80,7 +80,7 @@ function onFollowChange(user: Misskey.entities.UserDetailed) {
} }
async function onClick() { async function onClick() {
pleaseLogin(undefined, { type: 'web', path: `/@${props.user.username}@${props.user.host ?? host}` }); pleaseLogin({ openOnRemote: { type: 'web', path: `/@${props.user.username}@${props.user.host ?? host}` } });
wait.value = true; wait.value = true;

View File

@ -227,6 +227,7 @@ const emit = defineEmits<{
}>(); }>();
const inTimeline = inject<boolean>('inTimeline', false); const inTimeline = inject<boolean>('inTimeline', false);
const tl_withSensitive = inject<Ref<boolean>>('tl_withSensitive', ref(false));
const inChannel = inject('inChannel', null); const inChannel = inject('inChannel', null);
const currentClip = inject<Ref<Misskey.entities.Clip> | null>('currentClip', null); const currentClip = inject<Ref<Misskey.entities.Clip> | null>('currentClip', null);
@ -299,7 +300,7 @@ function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string
if (checkOnly) return false; if (checkOnly) return false;
if (inTimeline && !defaultStore.state.tl.filter.withSensitive && noteToCheck.files?.some((v) => v.isSensitive)) return 'sensitiveMute'; if (inTimeline && !tl_withSensitive.value && noteToCheck.files?.some((v) => v.isSensitive)) return 'sensitiveMute';
return false; return false;
} }
@ -419,7 +420,7 @@ if (!props.mock) {
} }
function renote(viaKeyboard = false) { function renote(viaKeyboard = false) {
pleaseLogin(undefined, pleaseLoginContext.value); pleaseLogin({ openOnRemote: pleaseLoginContext.value });
showMovedDialog(); showMovedDialog();
const { menu } = getRenoteMenu({ note: note.value, renoteButton, mock: props.mock }); const { menu } = getRenoteMenu({ note: note.value, renoteButton, mock: props.mock });
@ -429,7 +430,7 @@ function renote(viaKeyboard = false) {
} }
function reply(): void { function reply(): void {
pleaseLogin(undefined, pleaseLoginContext.value); pleaseLogin({ openOnRemote: pleaseLoginContext.value });
if (props.mock) { if (props.mock) {
return; return;
} }
@ -442,7 +443,7 @@ function reply(): void {
} }
function react(): void { function react(): void {
pleaseLogin(undefined, pleaseLoginContext.value); pleaseLogin({ openOnRemote: pleaseLoginContext.value });
showMovedDialog(); showMovedDialog();
if (appearNote.value.reactionAcceptance === 'likeOnly') { if (appearNote.value.reactionAcceptance === 'likeOnly') {
sound.playMisskeySfx('reaction'); sound.playMisskeySfx('reaction');
@ -563,7 +564,7 @@ function showRenoteMenu(): void {
} }
if (isMyRenote) { if (isMyRenote) {
pleaseLogin(undefined, pleaseLoginContext.value); pleaseLogin({ openOnRemote: pleaseLoginContext.value });
os.popupMenu([ os.popupMenu([
getCopyNoteLinkMenu(note.value, i18n.ts.copyLinkRenote), getCopyNoteLinkMenu(note.value, i18n.ts.copyLinkRenote),
{ type: 'divider' }, { type: 'divider' },

View File

@ -207,6 +207,7 @@ import { computed, inject, onMounted, provide, ref, shallowRef } from 'vue';
import * as mfm from 'mfm-js'; import * as mfm from 'mfm-js';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import { isLink } from '@@/js/is-link.js'; import { isLink } from '@@/js/is-link.js';
import { host } from '@@/js/config.js';
import MkNoteSub from '@/components/MkNoteSub.vue'; import MkNoteSub from '@/components/MkNoteSub.vue';
import MkNoteSimple from '@/components/MkNoteSimple.vue'; import MkNoteSimple from '@/components/MkNoteSimple.vue';
import MkReactionsViewer from '@/components/MkReactionsViewer.vue'; import MkReactionsViewer from '@/components/MkReactionsViewer.vue';
@ -230,7 +231,6 @@ import { reactionPicker } from '@/scripts/reaction-picker.js';
import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm.js'; import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm.js';
import { $i } from '@/account.js'; import { $i } from '@/account.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { host } from '@@/js/config.js';
import { getNoteClipMenu, getNoteMenu, getRenoteMenu } from '@/scripts/get-note-menu.js'; import { getNoteClipMenu, getNoteMenu, getRenoteMenu } from '@/scripts/get-note-menu.js';
import { useNoteCapture } from '@/scripts/use-note-capture.js'; import { useNoteCapture } from '@/scripts/use-note-capture.js';
import { deepClone } from '@/scripts/clone.js'; import { deepClone } from '@/scripts/clone.js';
@ -404,7 +404,7 @@ if (appearNote.value.reactionAcceptance === 'likeOnly') {
} }
function renote() { function renote() {
pleaseLogin(undefined, pleaseLoginContext.value); pleaseLogin({ openOnRemote: pleaseLoginContext.value });
showMovedDialog(); showMovedDialog();
const { menu } = getRenoteMenu({ note: note.value, renoteButton }); const { menu } = getRenoteMenu({ note: note.value, renoteButton });
@ -412,7 +412,7 @@ function renote() {
} }
function reply(): void { function reply(): void {
pleaseLogin(undefined, pleaseLoginContext.value); pleaseLogin({ openOnRemote: pleaseLoginContext.value });
showMovedDialog(); showMovedDialog();
os.post({ os.post({
reply: appearNote.value, reply: appearNote.value,
@ -423,7 +423,7 @@ function reply(): void {
} }
function react(): void { function react(): void {
pleaseLogin(undefined, pleaseLoginContext.value); pleaseLogin({ openOnRemote: pleaseLoginContext.value });
showMovedDialog(); showMovedDialog();
if (appearNote.value.reactionAcceptance === 'likeOnly') { if (appearNote.value.reactionAcceptance === 'likeOnly') {
sound.playMisskeySfx('reaction'); sound.playMisskeySfx('reaction');
@ -499,7 +499,7 @@ async function clip(): Promise<void> {
function showRenoteMenu(): void { function showRenoteMenu(): void {
if (!isMyRenote) return; if (!isMyRenote) return;
pleaseLogin(undefined, pleaseLoginContext.value); pleaseLogin({ openOnRemote: pleaseLoginContext.value });
os.popupMenu([{ os.popupMenu([{
text: i18n.ts.unrenote, text: i18n.ts.unrenote,
icon: 'ti ti-trash', icon: 'ti ti-trash',

View File

@ -29,14 +29,14 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup> <script lang="ts" setup>
import { computed, ref } from 'vue'; import { computed, ref } from 'vue';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import { host } from '@@/js/config.js';
import { useInterval } from '@@/js/use-interval.js';
import type { OpenOnRemoteOptions } from '@/scripts/please-login.js'; import type { OpenOnRemoteOptions } from '@/scripts/please-login.js';
import { sum } from '@/scripts/array.js'; import { sum } from '@/scripts/array.js';
import { pleaseLogin } from '@/scripts/please-login.js'; import { pleaseLogin } from '@/scripts/please-login.js';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js'; import { misskeyApi } from '@/scripts/misskey-api.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { host } from '@@/js/config.js';
import { useInterval } from '@@/js/use-interval.js';
const props = defineProps<{ const props = defineProps<{
noteId: string; noteId: string;
@ -85,7 +85,7 @@ if (props.poll.expiresAt) {
const vote = async (id) => { const vote = async (id) => {
if (props.readOnly || closed.value || isVoted.value) return; if (props.readOnly || closed.value || isVoted.value) return;
pleaseLogin(undefined, pleaseLoginContext.value); pleaseLogin({ openOnRemote: pleaseLoginContext.value });
const { canceled } = await os.confirm({ const { canceled } = await os.confirm({
type: 'question', type: 'question',

View File

@ -65,10 +65,10 @@ SPDX-License-Identifier: AGPL-3.0-only
</div> </div>
</div> </div>
<MkInfo v-if="hasNotSpecifiedMentions" warn :class="$style.hasNotSpecifiedMentions">{{ i18n.ts.notSpecifiedMentionWarning }} - <button class="_textButton" @click="addMissingMention()">{{ i18n.ts.add }}</button></MkInfo> <MkInfo v-if="hasNotSpecifiedMentions" warn :class="$style.hasNotSpecifiedMentions">{{ i18n.ts.notSpecifiedMentionWarning }} - <button class="_textButton" @click="addMissingMention()">{{ i18n.ts.add }}</button></MkInfo>
<input v-show="useCw" ref="cwInputEl" v-model="cw" :class="$style.cw" :placeholder="i18n.ts.annotation" @keydown="onKeydown"> <input v-show="useCw" ref="cwInputEl" v-model="cw" :class="$style.cw" :placeholder="i18n.ts.annotation" @keydown="onKeydown" @keyup="onKeyup" @compositionend="onCompositionEnd">
<div :class="[$style.textOuter, { [$style.withCw]: useCw }]"> <div :class="[$style.textOuter, { [$style.withCw]: useCw }]">
<div v-if="channel" :class="$style.colorBar" :style="{ background: channel.color }"></div> <div v-if="channel" :class="$style.colorBar" :style="{ background: channel.color }"></div>
<textarea ref="textareaEl" v-model="text" :class="[$style.text]" :disabled="posting || posted" :readonly="textAreaReadOnly" :placeholder="placeholder" data-cy-post-form-text @keydown="onKeydown" @paste="onPaste" @compositionupdate="onCompositionUpdate" @compositionend="onCompositionEnd"/> <textarea ref="textareaEl" v-model="text" :class="[$style.text]" :disabled="posting || posted" :readonly="textAreaReadOnly" :placeholder="placeholder" data-cy-post-form-text @keydown="onKeydown" @keyup="onKeyup" @paste="onPaste" @compositionupdate="onCompositionUpdate" @compositionend="onCompositionEnd"/>
<div v-if="maxTextLength - textLength < 100" :class="['_acrylic', $style.textCount, { [$style.textOver]: textLength > maxTextLength }]">{{ maxTextLength - textLength }}</div> <div v-if="maxTextLength - textLength < 100" :class="['_acrylic', $style.textCount, { [$style.textOver]: textLength > maxTextLength }]">{{ maxTextLength - textLength }}</div>
</div> </div>
<input v-show="withHashtags" ref="hashtagsInputEl" v-model="hashtags" :class="$style.hashtags" :placeholder="i18n.ts.hashtags" list="hashtags"> <input v-show="withHashtags" ref="hashtagsInputEl" v-model="hashtags" :class="$style.hashtags" :placeholder="i18n.ts.hashtags" list="hashtags">
@ -202,6 +202,7 @@ const recentHashtags = ref(JSON.parse(miLocalStorage.getItem('hashtags') ?? '[]'
const imeText = ref(''); const imeText = ref('');
const showingOptions = ref(false); const showingOptions = ref(false);
const textAreaReadOnly = ref(false); const textAreaReadOnly = ref(false);
const justEndedComposition = ref(false);
const draftKey = computed((): string => { const draftKey = computed((): string => {
let key = props.channel ? `channel:${props.channel.id}` : ''; let key = props.channel ? `channel:${props.channel.id}` : '';
@ -574,7 +575,13 @@ function clear() {
function onKeydown(ev: KeyboardEvent) { function onKeydown(ev: KeyboardEvent) {
if (ev.key === 'Enter' && (ev.ctrlKey || ev.metaKey) && canPost.value) post(); if (ev.key === 'Enter' && (ev.ctrlKey || ev.metaKey) && canPost.value) post();
if (ev.key === 'Escape') emit('esc'); // justEndedComposition.value is for Safari, which keyDown occurs after compositionend.
// ev.isComposing is for another browsers.
if (ev.key === 'Escape' && !justEndedComposition.value && !ev.isComposing) emit('esc');
}
function onKeyup(ev: KeyboardEvent) {
justEndedComposition.value = false;
} }
function onCompositionUpdate(ev: CompositionEvent) { function onCompositionUpdate(ev: CompositionEvent) {
@ -583,6 +590,7 @@ function onCompositionUpdate(ev: CompositionEvent) {
function onCompositionEnd(ev: CompositionEvent) { function onCompositionEnd(ev: CompositionEvent) {
imeText.value = ''; imeText.value = '';
justEndedComposition.value = true;
} }
async function onPaste(ev: ClipboardEvent) { async function onPaste(ev: ClipboardEvent) {

View File

@ -16,9 +16,8 @@ SPDX-License-Identifier: AGPL-3.0-only
@keydown.space.enter="show" @keydown.space.enter="show"
> >
<div ref="prefixEl" :class="$style.prefix"><slot name="prefix"></slot></div> <div ref="prefixEl" :class="$style.prefix"><slot name="prefix"></slot></div>
<select <div
ref="inputEl" ref="inputEl"
v-model="v"
v-adaptive-border v-adaptive-border
tabindex="-1" tabindex="-1"
:class="$style.inputCore" :class="$style.inputCore"
@ -26,27 +25,25 @@ SPDX-License-Identifier: AGPL-3.0-only
:required="required" :required="required"
:readonly="readonly" :readonly="readonly"
:placeholder="placeholder" :placeholder="placeholder"
@input="onInput"
@mousedown.prevent="() => {}" @mousedown.prevent="() => {}"
@keydown.prevent="() => {}" @keydown.prevent="() => {}"
> >
<div style="pointer-events: none;">{{ currentValueText ?? '' }}</div>
<div style="display: none;">
<slot></slot> <slot></slot>
</select> </div>
</div>
<div ref="suffixEl" :class="$style.suffix"><i class="ti ti-chevron-down" :class="[$style.chevron, { [$style.chevronOpening]: opening }]"></i></div> <div ref="suffixEl" :class="$style.suffix"><i class="ti ti-chevron-down" :class="[$style.chevron, { [$style.chevronOpening]: opening }]"></i></div>
</div> </div>
<div :class="$style.caption"><slot name="caption"></slot></div> <div :class="$style.caption"><slot name="caption"></slot></div>
<MkButton v-if="manualSave && changed" primary :class="$style.save" @click="updated"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted, nextTick, ref, watch, computed, toRefs, VNode, useSlots, VNodeChild } from 'vue'; import { onMounted, nextTick, ref, watch, computed, toRefs, VNode, useSlots, VNodeChild } from 'vue';
import MkButton from '@/components/MkButton.vue';
import * as os from '@/os.js';
import { useInterval } from '@@/js/use-interval.js'; import { useInterval } from '@@/js/use-interval.js';
import { i18n } from '@/i18n.js';
import type { MenuItem } from '@/types/menu.js'; import type { MenuItem } from '@/types/menu.js';
import * as os from '@/os.js';
const props = defineProps<{ const props = defineProps<{
modelValue: string | null; modelValue: string | null;
@ -56,25 +53,20 @@ const props = defineProps<{
placeholder?: string; placeholder?: string;
autofocus?: boolean; autofocus?: boolean;
inline?: boolean; inline?: boolean;
manualSave?: boolean;
small?: boolean; small?: boolean;
large?: boolean; large?: boolean;
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
(ev: 'changeByUser', value: string | null): void; (ev: 'update:modelValue', value: string | number | null): void;
(ev: 'update:modelValue', value: string | null): void;
}>(); }>();
const slots = useSlots(); const slots = useSlots();
const { modelValue, autofocus } = toRefs(props); const { modelValue, autofocus } = toRefs(props);
const v = ref(modelValue.value);
const focused = ref(false); const focused = ref(false);
const opening = ref(false); const opening = ref(false);
const changed = ref(false); const currentValueText = ref<string | null>(null);
const invalid = ref(false);
const filled = computed(() => v.value !== '' && v.value != null);
const inputEl = ref<HTMLObjectElement | null>(null); const inputEl = ref<HTMLObjectElement | null>(null);
const prefixEl = ref<HTMLElement | null>(null); const prefixEl = ref<HTMLElement | null>(null);
const suffixEl = ref<HTMLElement | null>(null); const suffixEl = ref<HTMLElement | null>(null);
@ -85,26 +77,6 @@ const height =
36; 36;
const focus = () => container.value?.focus(); const focus = () => container.value?.focus();
const onInput = (ev) => {
changed.value = true;
};
const updated = () => {
changed.value = false;
emit('update:modelValue', v.value);
};
watch(modelValue, newValue => {
v.value = newValue;
});
watch(v, () => {
if (!props.manualSave) {
updated();
}
invalid.value = inputEl.value?.validity.badInput ?? true;
});
// //
// 0 // 0
@ -134,6 +106,31 @@ onMounted(() => {
}); });
}); });
watch(modelValue, () => {
const scanOptions = (options: VNodeChild[]) => {
for (const vnode of options) {
if (typeof vnode !== 'object' || vnode === null || Array.isArray(vnode)) continue;
if (vnode.type === 'optgroup') {
const optgroup = vnode;
if (Array.isArray(optgroup.children)) scanOptions(optgroup.children);
} else if (Array.isArray(vnode.children)) { //
const fragment = vnode;
if (Array.isArray(fragment.children)) scanOptions(fragment.children);
} else if (vnode.props == null) { // v-if false
// nop?
} else {
const option = vnode;
if (option.props?.value === modelValue.value) {
currentValueText.value = option.children as string;
break;
}
}
}
};
scanOptions(slots.default!());
}, { immediate: true });
function show() { function show() {
if (opening.value) return; if (opening.value) return;
focus(); focus();
@ -146,11 +143,9 @@ function show() {
const pushOption = (option: VNode) => { const pushOption = (option: VNode) => {
menu.push({ menu.push({
text: option.children as string, text: option.children as string,
active: computed(() => v.value === option.props?.value), active: computed(() => modelValue.value === option.props?.value),
action: () => { action: () => {
v.value = option.props?.value; emit('update:modelValue', option.props?.value);
changed.value = true;
emit('changeByUser', v.value);
}, },
}); });
}; };
@ -248,7 +243,8 @@ function show() {
.inputCore { .inputCore {
appearance: none; appearance: none;
-webkit-appearance: none; -webkit-appearance: none;
display: block; display: flex;
align-items: center;
height: v-bind("height + 'px'"); height: v-bind("height + 'px'");
width: 100%; width: 100%;
margin: 0; margin: 0;

View File

@ -38,6 +38,7 @@ const props = withDefaults(defineProps<{
sound?: boolean; sound?: boolean;
withRenotes?: boolean; withRenotes?: boolean;
withReplies?: boolean; withReplies?: boolean;
withSensitive?: boolean;
onlyFiles?: boolean; onlyFiles?: boolean;
}>(), { }>(), {
withRenotes: true, withRenotes: true,
@ -51,6 +52,7 @@ const emit = defineEmits<{
}>(); }>();
provide('inTimeline', true); provide('inTimeline', true);
provide('tl_withSensitive', computed(() => props.withSensitive));
provide('inChannel', computed(() => props.src === 'channel')); provide('inChannel', computed(() => props.src === 'channel'));
type TimelineQueryType = { type TimelineQueryType = {
@ -248,6 +250,9 @@ function refreshEndpointAndChannel() {
// IDTL // IDTL
watch(() => [props.list, props.antenna, props.channel, props.role, props.withRenotes], refreshEndpointAndChannel); watch(() => [props.list, props.antenna, props.channel, props.role, props.withRenotes], refreshEndpointAndChannel);
// withSensitiveOK
watch(() => props.withSensitive, reloadTimeline);
// //
refreshEndpointAndChannel(); refreshEndpointAndChannel();

View File

@ -53,7 +53,7 @@ export type Tab = {
</script> </script>
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted, onUnmounted, watch, nextTick, shallowRef } from 'vue'; import { nextTick, onMounted, onUnmounted, shallowRef, watch } from 'vue';
import { defaultStore } from '@/store.js'; import { defaultStore } from '@/store.js';
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
@ -120,14 +120,14 @@ function onTabWheel(ev: WheelEvent) {
let entering = false; let entering = false;
async function enter(element: Element) { async function enter(el: Element) {
if (!(el instanceof HTMLElement)) return;
entering = true; entering = true;
const el = element as HTMLElement;
const elementWidth = el.getBoundingClientRect().width; const elementWidth = el.getBoundingClientRect().width;
el.style.width = '0'; el.style.width = '0';
el.style.paddingLeft = '0'; el.style.paddingLeft = '0';
el.offsetWidth; // force reflow el.offsetWidth; // reflow
el.style.width = elementWidth + 'px'; el.style.width = `${elementWidth}px`;
el.style.paddingLeft = ''; el.style.paddingLeft = '';
nextTick(() => { nextTick(() => {
entering = false; entering = false;
@ -136,22 +136,23 @@ async function enter(element: Element) {
setTimeout(renderTab, 170); setTimeout(renderTab, 170);
} }
function afterEnter(element: Element) { function afterEnter(el: Element) {
//el.style.width = ''; if (!(el instanceof HTMLElement)) return;
// element.style.width = '';
} }
async function leave(element: Element) { async function leave(el: Element) {
const el = element as HTMLElement; if (!(el instanceof HTMLElement)) return;
const elementWidth = el.getBoundingClientRect().width; const elementWidth = el.getBoundingClientRect().width;
el.style.width = elementWidth + 'px'; el.style.width = `${elementWidth}px`;
el.style.paddingLeft = ''; el.style.paddingLeft = '';
el.offsetWidth; // force reflow el.offsetWidth; // reflow
el.style.width = '0'; el.style.width = '0';
el.style.paddingLeft = '0'; el.style.paddingLeft = '0';
} }
function afterLeave(element: Element) { function afterLeave(el: Element) {
const el = element as HTMLElement; if (!(el instanceof HTMLElement)) return;
el.style.width = ''; el.style.width = '';
} }

View File

@ -4,19 +4,11 @@
*/ */
import { Directive } from 'vue'; import { Directive } from 'vue';
import { getBgColor } from '@/scripts/get-bg-color.js';
export default { export default {
mounted(src, binding, vn) { mounted(src, binding, vn) {
const getBgColor = (el: HTMLElement) => { const parentBg = getBgColor(src.parentElement) ?? 'transparent';
const style = window.getComputedStyle(el);
if (style.backgroundColor && !['rgba(0, 0, 0, 0)', 'rgba(0,0,0,0)', 'transparent'].includes(style.backgroundColor)) {
return style.backgroundColor;
} else {
return el.parentElement ? getBgColor(el.parentElement) : 'transparent';
}
};
const parentBg = getBgColor(src.parentElement);
const myBg = window.getComputedStyle(src).backgroundColor; const myBg = window.getComputedStyle(src).backgroundColor;

View File

@ -4,19 +4,11 @@
*/ */
import { Directive } from 'vue'; import { Directive } from 'vue';
import { getBgColor } from '@/scripts/get-bg-color.js';
export default { export default {
mounted(src, binding, vn) { mounted(src, binding, vn) {
const getBgColor = (el: HTMLElement) => { const parentBg = getBgColor(src.parentElement) ?? 'transparent';
const style = window.getComputedStyle(el);
if (style.backgroundColor && !['rgba(0, 0, 0, 0)', 'rgba(0,0,0,0)', 'transparent'].includes(style.backgroundColor)) {
return style.backgroundColor;
} else {
return el.parentElement ? getBgColor(el.parentElement) : 'transparent';
}
};
const parentBg = getBgColor(src.parentElement);
const myBg = window.getComputedStyle(src).backgroundColor; const myBg = window.getComputedStyle(src).backgroundColor;

View File

@ -4,19 +4,11 @@
*/ */
import { Directive } from 'vue'; import { Directive } from 'vue';
import { getBgColor } from '@/scripts/get-bg-color.js';
export default { export default {
mounted(src, binding, vn) { mounted(src, binding, vn) {
const getBgColor = (el: HTMLElement) => { const parentBg = getBgColor(src.parentElement) ?? 'transparent';
const style = window.getComputedStyle(el);
if (style.backgroundColor && !['rgba(0, 0, 0, 0)', 'rgba(0,0,0,0)', 'transparent'].includes(style.backgroundColor)) {
return style.backgroundColor;
} else {
return el.parentElement ? getBgColor(el.parentElement) : 'transparent';
}
};
const parentBg = getBgColor(src.parentElement);
const myBg = getComputedStyle(document.documentElement).getPropertyValue('--MI_THEME-panel'); const myBg = getComputedStyle(document.documentElement).getPropertyValue('--MI_THEME-panel');

View File

@ -688,14 +688,16 @@ export function contextMenu(items: MenuItem[], ev: MouseEvent): Promise<void> {
} }
export function post(props: Record<string, any> = {}): Promise<void> { export function post(props: Record<string, any> = {}): Promise<void> {
pleaseLogin(undefined, (props.initialText || props.initialNote ? { pleaseLogin({
openOnRemote: (props.initialText || props.initialNote ? {
type: 'share', type: 'share',
params: { params: {
text: props.initialText ?? props.initialNote.text, text: props.initialText ?? props.initialNote.text,
visibility: props.initialVisibility ?? props.initialNote?.visibility, visibility: props.initialVisibility ?? props.initialNote?.visibility,
localOnly: (props.initialLocalOnly || props.initialNote?.localOnly) ? '1' : '0', localOnly: (props.initialLocalOnly || props.initialNote?.localOnly) ? '1' : '0',
}, },
} : undefined)); } : undefined),
});
showMovedDialog(); showMovedDialog();
return new Promise(resolve => { return new Promise(resolve => {

View File

@ -55,7 +55,7 @@ import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js'; import { misskeyApi } from '@/scripts/misskey-api.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js'; import { definePageMetadata } from '@/scripts/page-metadata.js';
import { $i, updateAccount } from '@/account.js'; import { $i, updateAccountPartial } from '@/account.js';
import { defaultStore } from '@/store.js'; import { defaultStore } from '@/store.js';
const props = defineProps<{ const props = defineProps<{
@ -90,7 +90,7 @@ async function read(target: Misskey.entities.Announcement): Promise<void> {
target.isRead = true; target.isRead = true;
await misskeyApi('i/read-announcement', { announcementId: target.id }); await misskeyApi('i/read-announcement', { announcementId: target.id });
if ($i) { if ($i) {
updateAccount({ updateAccountPartial({
unreadAnnouncements: $i.unreadAnnouncements.filter((a: { id: string; }) => a.id !== target.id), unreadAnnouncements: $i.unreadAnnouncements.filter((a: { id: string; }) => a.id !== target.id),
}); });
} }

View File

@ -56,7 +56,7 @@ import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js'; import { misskeyApi } from '@/scripts/misskey-api.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js'; import { definePageMetadata } from '@/scripts/page-metadata.js';
import { $i, updateAccount } from '@/account.js'; import { $i, updateAccountPartial } from '@/account.js';
const paginationCurrent = { const paginationCurrent = {
endpoint: 'announcements' as const, endpoint: 'announcements' as const,
@ -94,7 +94,7 @@ async function read(target) {
return a; return a;
}); });
misskeyApi('i/read-announcement', { announcementId: target.id }); misskeyApi('i/read-announcement', { announcementId: target.id });
updateAccount({ updateAccountPartial({
unreadAnnouncements: $i!.unreadAnnouncements.filter(a => a.id !== target.id), unreadAnnouncements: $i!.unreadAnnouncements.filter(a => a.id !== target.id),
}); });
} }

View File

@ -5,10 +5,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<template> <template>
<MkStickyContainer> <MkStickyContainer>
<template #header> <template #header><MkPageHeader/></template>
<MkPageHeader/> <MkSpacer v-if="!instance.disableRegistration || !($i && ($i.isAdmin || $i.policies.canInvite))" :contentMax="1200">
</template>
<MKSpacer v-if="!instance.disableRegistration || !($i && ($i.isAdmin || $i.policies.canInvite))" :contentMax="1200">
<div :class="$style.root"> <div :class="$style.root">
<img :class="$style.img" :src="serverErrorImageUrl" class="_ghost"/> <img :class="$style.img" :src="serverErrorImageUrl" class="_ghost"/>
<div :class="$style.text"> <div :class="$style.text">
@ -16,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ i18n.ts.nothing }} {{ i18n.ts.nothing }}
</div> </div>
</div> </div>
</MKSpacer> </MkSpacer>
<MkSpacer v-else :contentMax="800"> <MkSpacer v-else :contentMax="800">
<div class="_gaps_m" style="text-align: center;"> <div class="_gaps_m" style="text-align: center;">
<div v-if="resetCycle && inviteLimit">{{ i18n.tsx.inviteLimitResetCycle({ time: resetCycle, limit: inviteLimit }) }}</div> <div v-if="resetCycle && inviteLimit">{{ i18n.tsx.inviteLimitResetCycle({ time: resetCycle, limit: inviteLimit }) }}</div>

View File

@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template> <template>
<MkStickyContainer> <MkStickyContainer>
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MKSpacer v-if="!(typeof error === 'undefined')" :contentMax="1200"> <MkSpacer v-if="error != null" :contentMax="1200">
<div :class="$style.root"> <div :class="$style.root">
<img :class="$style.img" :src="serverErrorImageUrl" class="_ghost"/> <img :class="$style.img" :src="serverErrorImageUrl" class="_ghost"/>
<p :class="$style.text"> <p :class="$style.text">
@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ i18n.ts.nothing }} {{ i18n.ts.nothing }}
</p> </p>
</div> </div>
</MKSpacer> </MkSpacer>
<MkSpacer v-else-if="list" :contentMax="700" :class="$style.main"> <MkSpacer v-else-if="list" :contentMax="700" :class="$style.main">
<div v-if="list" class="members _margin"> <div v-if="list" class="members _margin">
<div :class="$style.member_text">{{ i18n.ts.members }}</div> <div :class="$style.member_text">{{ i18n.ts.members }}</div>
@ -50,7 +50,7 @@ const props = defineProps<{
}>(); }>();
const list = ref<Misskey.entities.UserList | null>(null); const list = ref<Misskey.entities.UserList | null>(null);
const error = ref(); const error = ref<unknown | null>(null);
const users = ref<Misskey.entities.UserDetailed[]>([]); const users = ref<Misskey.entities.UserDetailed[]>([]);
function fetchList(): void { function fetchList(): void {

View File

@ -24,7 +24,7 @@ const props = defineProps<{
}>(); }>();
if (props.showLoginPopup) { if (props.showLoginPopup) {
pleaseLogin('/'); pleaseLogin({ path: '/' });
} }
const headerActions = computed(() => []); const headerActions = computed(() => []);

View File

@ -61,6 +61,7 @@ import { i18n } from '@/i18n.js';
import { dateString } from '@/filters/date.js'; import { dateString } from '@/filters/date.js';
import MkClipPreview from '@/components/MkClipPreview.vue'; import MkClipPreview from '@/components/MkClipPreview.vue';
import { defaultStore } from '@/store.js'; import { defaultStore } from '@/store.js';
import { pleaseLogin } from '@/scripts/please-login.js';
const props = defineProps<{ const props = defineProps<{
noteId: string; noteId: string;
@ -128,6 +129,11 @@ function fetchNote() {
}); });
} }
}).catch(err => { }).catch(err => {
if (err.id === '8e75455b-738c-471d-9f80-62693f33372e') {
pleaseLogin({
message: i18n.ts.thisContentsAreMarkedAsSigninRequiredByAuthor,
});
}
error.value = err; error.value = err;
}); });
} }

View File

@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template> <template>
<MkStickyContainer> <MkStickyContainer>
<template #header><MkPageHeader v-model:tab="tab" :tabs="headerTabs"/></template> <template #header><MkPageHeader v-model:tab="tab" :tabs="headerTabs"/></template>
<MKSpacer v-if="!(typeof error === 'undefined')" :contentMax="1200"> <MkSpacer v-if="error != null" :contentMax="1200">
<div :class="$style.root"> <div :class="$style.root">
<img :class="$style.img" :src="serverErrorImageUrl" class="_ghost"/> <img :class="$style.img" :src="serverErrorImageUrl" class="_ghost"/>
<p :class="$style.text"> <p :class="$style.text">
@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ error }} {{ error }}
</p> </p>
</div> </div>
</MKSpacer> </MkSpacer>
<MkSpacer v-else-if="tab === 'users'" :contentMax="1200"> <MkSpacer v-else-if="tab === 'users'" :contentMax="1200">
<div class="_gaps_s"> <div class="_gaps_s">
<div v-if="role">{{ role.description }}</div> <div v-if="role">{{ role.description }}</div>
@ -26,7 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div> </div>
</MkSpacer> </MkSpacer>
<MkSpacer v-else-if="tab === 'timeline'" :contentMax="700"> <MkSpacer v-else-if="tab === 'timeline'" :contentMax="700">
<MkTimeline v-if="visible" ref="timeline" src="role" :role="props.role"/> <MkTimeline v-if="visible" ref="timeline" src="role" :role="props.roleId"/>
<div v-else-if="!visible" class="_fullinfo"> <div v-else-if="!visible" class="_fullinfo">
<img :src="infoImageUrl" class="_ghost"/> <img :src="infoImageUrl" class="_ghost"/>
<div>{{ i18n.ts.nothing }}</div> <div>{{ i18n.ts.nothing }}</div>
@ -47,23 +47,24 @@ import { instanceName } from '@@/js/config.js';
import { serverErrorImageUrl, infoImageUrl } from '@/instance.js'; import { serverErrorImageUrl, infoImageUrl } from '@/instance.js';
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
role: string; roleId: string;
initialTab?: string; initialTab?: string;
}>(), { }>(), {
initialTab: 'users', initialTab: 'users',
}); });
// eslint-disable-next-line vue/no-setup-props-reactivity-loss
const tab = ref(props.initialTab); const tab = ref(props.initialTab);
const role = ref<Misskey.entities.Role>(); const role = ref<Misskey.entities.Role | null>(null);
const error = ref(); const error = ref<string | null>(null);
const visible = ref(false); const visible = ref(false);
watch(() => props.role, () => { watch(() => props.roleId, () => {
misskeyApi('roles/show', { misskeyApi('roles/show', {
roleId: props.role, roleId: props.roleId,
}).then(res => { }).then(res => {
role.value = res; role.value = res;
document.title = `${role.value.name} | ${instanceName}`; error.value = null;
visible.value = res.isExplorable && res.isPublic; visible.value = res.isExplorable && res.isPublic;
}).catch((err) => { }).catch((err) => {
if (err.code === 'NO_SUCH_ROLE') { if (err.code === 'NO_SUCH_ROLE') {
@ -71,7 +72,6 @@ watch(() => props.role, () => {
} else { } else {
error.value = i18n.ts.somethingHappened; error.value = i18n.ts.somethingHappened;
} }
document.title = `${error.value} | ${instanceName}`;
}); });
}, { immediate: true }); }, { immediate: true });
@ -79,7 +79,7 @@ const users = computed(() => ({
endpoint: 'roles/users' as const, endpoint: 'roles/users' as const,
limit: 30, limit: 30,
params: { params: {
roleId: props.role, roleId: props.roleId,
}, },
})); }));
@ -94,7 +94,7 @@ const headerTabs = computed(() => [{
}]); }]);
definePageMetadata(() => ({ definePageMetadata(() => ({
title: role.value ? role.value.name : i18n.ts.role, title: role.value ? role.value.name : (error.value ?? i18n.ts.role),
icon: 'ti ti-badge', icon: 'ti ti-badge',
})); }));
</script> </script>

View File

@ -84,7 +84,7 @@ import FormSection from '@/components/form/section.vue';
import MkFolder from '@/components/MkFolder.vue'; import MkFolder from '@/components/MkFolder.vue';
import MkLink from '@/components/MkLink.vue'; import MkLink from '@/components/MkLink.vue';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { signinRequired, updateAccount } from '@/account.js'; import { signinRequired, updateAccountPartial } from '@/account.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
const $i = signinRequired(); const $i = signinRequired();
@ -123,7 +123,7 @@ async function unregisterTOTP(): Promise<void> {
password: auth.result.password, password: auth.result.password,
token: auth.result.token, token: auth.result.token,
}).then(res => { }).then(res => {
updateAccount({ updateAccountPartial({
twoFactorEnabled: false, twoFactorEnabled: false,
}); });
}).catch(error => { }).catch(error => {

View File

@ -6,13 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template> <template>
<div class="_gaps_m"> <div class="_gaps_m">
<MkSelect v-model="type"> <MkSelect v-model="type">
<option value="all">{{ i18n.ts.all }}</option> <option v-for="type in props.configurableTypes ?? notificationConfigTypes" :key="type" :value="type">{{ notificationConfigTypesI18nMap[type] }}</option>
<option value="following">{{ i18n.ts.following }}</option>
<option value="follower">{{ i18n.ts.followers }}</option>
<option value="mutualFollow">{{ i18n.ts.mutualFollow }}</option>
<option value="followingOrFollower">{{ i18n.ts.followingOrFollower }}</option>
<option value="list">{{ i18n.ts.userList }}</option>
<option value="never">{{ i18n.ts.none }}</option>
</MkSelect> </MkSelect>
<MkSelect v-if="type === 'list'" v-model="userListId"> <MkSelect v-if="type === 'list'" v-model="userListId">
@ -21,31 +15,61 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkSelect> </MkSelect>
<div class="_buttons"> <div class="_buttons">
<MkButton inline primary @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton> <MkButton inline primary :disabled="type === 'list' && userListId === null" @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton>
</div> </div>
</div> </div>
</template> </template>
<script lang="ts">
const notificationConfigTypes = [
'all',
'following',
'follower',
'mutualFollow',
'followingOrFollower',
'list',
'never'
] as const;
export type NotificationConfig = {
type: Exclude<typeof notificationConfigTypes[number], 'list'>;
} | {
type: 'list';
userListId: string;
};
</script>
<script lang="ts" setup> <script lang="ts" setup>
import { ref } from 'vue';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import { ref } from 'vue';
import MkSelect from '@/components/MkSelect.vue'; import MkSelect from '@/components/MkSelect.vue';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
const props = defineProps<{ const props = defineProps<{
value: any; value: NotificationConfig;
userLists: Misskey.entities.UserList[]; userLists: Misskey.entities.UserList[];
configurableTypes?: NotificationConfig['type'][]; // If not specified, all types are configurable
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
(ev: 'update', result: any): void; (ev: 'update', result: NotificationConfig): void;
}>(); }>();
const notificationConfigTypesI18nMap: Record<typeof notificationConfigTypes[number], string> = {
all: i18n.ts.all,
following: i18n.ts.following,
follower: i18n.ts.followers,
mutualFollow: i18n.ts.mutualFollow,
followingOrFollower: i18n.ts.followingOrFollower,
list: i18n.ts.userList,
never: i18n.ts.none,
};
const type = ref(props.value.type); const type = ref(props.value.type);
const userListId = ref(props.value.userListId); const userListId = ref(props.value.type === 'list' ? props.value.userListId : null);
function save() { function save() {
emit('update', { type: type.value, userListId: userListId.value }); emit('update', type.value === 'list' ? { type: type.value, userListId: userListId.value! } : { type: type.value });
} }
</script> </script>

View File

@ -22,7 +22,12 @@ SPDX-License-Identifier: AGPL-3.0-only
}} }}
</template> </template>
<XNotificationConfig :userLists="userLists" :value="$i.notificationRecieveConfig[type] ?? { type: 'all' }" @update="(res) => updateReceiveConfig(type, res)"/> <XNotificationConfig
:userLists="userLists"
:value="$i.notificationRecieveConfig[type] ?? { type: 'all' }"
:configurableTypes="onlyOnOrOffNotificationTypes.includes(type) ? ['all', 'never'] : undefined"
@update="(res) => updateReceiveConfig(type, res)"
/>
</MkFolder> </MkFolder>
</div> </div>
</FormSection> </FormSection>
@ -58,7 +63,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup> <script lang="ts" setup>
import { shallowRef, computed } from 'vue'; import { shallowRef, computed } from 'vue';
import XNotificationConfig from './notifications.notification-config.vue'; import XNotificationConfig, { type NotificationConfig } from './notifications.notification-config.vue';
import FormLink from '@/components/form/link.vue'; import FormLink from '@/components/form/link.vue';
import FormSection from '@/components/form/section.vue'; import FormSection from '@/components/form/section.vue';
import MkFolder from '@/components/MkFolder.vue'; import MkFolder from '@/components/MkFolder.vue';
@ -73,7 +78,9 @@ import { notificationTypes } from '@@/js/const.js';
const $i = signinRequired(); const $i = signinRequired();
const nonConfigurableNotificationTypes = ['note', 'roleAssigned', 'followRequestAccepted', 'achievementEarned', 'test', 'exportCompleted'] as const satisfies (typeof notificationTypes[number])[]; const nonConfigurableNotificationTypes = ['note', 'roleAssigned', 'followRequestAccepted', 'test', 'exportCompleted'] satisfies (typeof notificationTypes[number])[] as string[];
const onlyOnOrOffNotificationTypes = ['app', 'achievementEarned', 'login'] satisfies (typeof notificationTypes[number])[] as string[];
const allowButton = shallowRef<InstanceType<typeof MkPushNotificationAllowButton>>(); const allowButton = shallowRef<InstanceType<typeof MkPushNotificationAllowButton>>();
const pushRegistrationInServer = computed(() => allowButton.value?.pushRegistrationInServer); const pushRegistrationInServer = computed(() => allowButton.value?.pushRegistrationInServer);
@ -88,7 +95,7 @@ async function readAllNotifications() {
await os.apiWithDialog('notifications/mark-all-as-read'); await os.apiWithDialog('notifications/mark-all-as-read');
} }
async function updateReceiveConfig(type, value) { async function updateReceiveConfig(type: typeof notificationTypes[number], value: NotificationConfig) {
await os.apiWithDialog('i/update', { await os.apiWithDialog('i/update', {
notificationRecieveConfig: { notificationRecieveConfig: {
...$i.notificationRecieveConfig, ...$i.notificationRecieveConfig,

View File

@ -36,7 +36,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #caption>{{ i18n.ts.noCrawleDescription }}</template> <template #caption>{{ i18n.ts.noCrawleDescription }}</template>
</MkSwitch> </MkSwitch>
<MkSwitch v-model="preventAiLearning" @update:modelValue="save()"> <MkSwitch v-model="preventAiLearning" @update:modelValue="save()">
{{ i18n.ts.preventAiLearning }}<span class="_beta">{{ i18n.ts.beta }}</span> {{ i18n.ts.preventAiLearning }}
<template #caption>{{ i18n.ts.preventAiLearningDescription }}</template> <template #caption>{{ i18n.ts.preventAiLearningDescription }}</template>
</MkSwitch> </MkSwitch>
<MkSwitch v-model="isExplorable" @update:modelValue="save()"> <MkSwitch v-model="isExplorable" @update:modelValue="save()">
@ -44,6 +44,21 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #caption>{{ i18n.ts.makeExplorableDescription }}</template> <template #caption>{{ i18n.ts.makeExplorableDescription }}</template>
</MkSwitch> </MkSwitch>
<FormSection>
<template #label>{{ i18n.ts.lockdown }}</template>
<div class="_gaps_m">
<MkSwitch v-model="requireSigninToViewContents" @update:modelValue="save()">
{{ i18n.ts._accountSettings.requireSigninToViewContents }}<span class="_beta">{{ i18n.ts.beta }}</span>
<template #caption>
<div>{{ i18n.ts._accountSettings.requireSigninToViewContentsDescription1 }}</div>
<div><i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i> {{ i18n.ts._accountSettings.requireSigninToViewContentsDescription2 }}</div>
<div><i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i> {{ i18n.ts._accountSettings.requireSigninToViewContentsDescription3 }}</div>
</template>
</MkSwitch>
</div>
</FormSection>
<FormSection> <FormSection>
<div class="_gaps_m"> <div class="_gaps_m">
<MkSwitch v-model="rememberNoteVisibility" @update:modelValue="save()">{{ i18n.ts.rememberNoteVisibility }}</MkSwitch> <MkSwitch v-model="rememberNoteVisibility" @update:modelValue="save()">{{ i18n.ts.rememberNoteVisibility }}</MkSwitch>
@ -90,6 +105,7 @@ const autoAcceptFollowed = ref($i.autoAcceptFollowed);
const noCrawle = ref($i.noCrawle); const noCrawle = ref($i.noCrawle);
const preventAiLearning = ref($i.preventAiLearning); const preventAiLearning = ref($i.preventAiLearning);
const isExplorable = ref($i.isExplorable); const isExplorable = ref($i.isExplorable);
const requireSigninToViewContents = ref($i.requireSigninToViewContents ?? false);
const hideOnlineStatus = ref($i.hideOnlineStatus); const hideOnlineStatus = ref($i.hideOnlineStatus);
const publicReactions = ref($i.publicReactions); const publicReactions = ref($i.publicReactions);
const followingVisibility = ref($i.followingVisibility); const followingVisibility = ref($i.followingVisibility);
@ -107,6 +123,7 @@ function save() {
noCrawle: !!noCrawle.value, noCrawle: !!noCrawle.value,
preventAiLearning: !!preventAiLearning.value, preventAiLearning: !!preventAiLearning.value,
isExplorable: !!isExplorable.value, isExplorable: !!isExplorable.value,
requireSigninToViewContents: !!requireSigninToViewContents.value,
hideOnlineStatus: !!hideOnlineStatus.value, hideOnlineStatus: !!hideOnlineStatus.value,
publicReactions: !!publicReactions.value, publicReactions: !!publicReactions.value,
followingVisibility: followingVisibility.value, followingVisibility: followingVisibility.value,

View File

@ -22,6 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:list="src.split(':')[1]" :list="src.split(':')[1]"
:withRenotes="withRenotes" :withRenotes="withRenotes"
:withReplies="withReplies" :withReplies="withReplies"
:withSensitive="withSensitive"
:onlyFiles="onlyFiles" :onlyFiles="onlyFiles"
:sound="true" :sound="true"
@queue="queueUpdated" @queue="queueUpdated"
@ -121,11 +122,6 @@ watch(src, () => {
queue.value = 0; queue.value = 0;
}); });
watch(withSensitive, () => {
//
tlComponent.value?.reloadTimeline();
});
function queueUpdated(q: number): void { function queueUpdated(q: number): void {
queue.value = q; queue.value = q;
} }

View File

@ -217,7 +217,7 @@ const routes: RouteDef[] = [{
component: page(() => import('@/pages/theme-editor.vue')), component: page(() => import('@/pages/theme-editor.vue')),
loginRequired: true, loginRequired: true,
}, { }, {
path: '/roles/:role', path: '/roles/:roleId',
component: page(() => import('@/pages/role.vue')), component: page(() => import('@/pages/role.vue')),
}, { }, {
path: '/user-tags/:tag', path: '/user-tags/:tag',

View File

@ -0,0 +1,18 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import tinycolor from 'tinycolor2';
export const getBgColor = (elem?: Element | null | undefined): string | null => {
if (elem == null) return null;
const { backgroundColor: bg } = window.getComputedStyle(elem);
if (bg && tinycolor(bg).getAlpha() !== 0) {
return bg;
}
return getBgColor(elem.parentElement);
};

View File

@ -44,17 +44,21 @@ export type OpenOnRemoteOptions = {
params: Record<string, string>; params: Record<string, string>;
}; };
export function pleaseLogin(path?: string, openOnRemote?: OpenOnRemoteOptions) { export function pleaseLogin(opts: {
path?: string;
message?: string;
openOnRemote?: OpenOnRemoteOptions;
} = {}) {
if ($i) return; if ($i) return;
const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkSigninDialog.vue')), { const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkSigninDialog.vue')), {
autoSet: true, autoSet: true,
message: openOnRemote ? i18n.ts.signinOrContinueOnRemote : i18n.ts.signinRequired, message: opts.message ?? (opts.openOnRemote ? i18n.ts.signinOrContinueOnRemote : i18n.ts.signinRequired),
openOnRemote, openOnRemote: opts.openOnRemote,
}, { }, {
cancelled: () => { cancelled: () => {
if (path) { if (opts.path) {
window.location.href = path; window.location.href = opts.path;
} }
}, },
closed: () => dispose(), closed: () => dispose(),

View File

@ -49,6 +49,7 @@ export type Column = {
tl?: BasicTimelineType; tl?: BasicTimelineType;
withRenotes?: boolean; withRenotes?: boolean;
withReplies?: boolean; withReplies?: boolean;
withSensitive?: boolean;
onlyFiles?: boolean; onlyFiles?: boolean;
soundSetting: SoundStore; soundSetting: SoundStore;
}; };

View File

@ -24,6 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:src="column.tl" :src="column.tl"
:withRenotes="withRenotes" :withRenotes="withRenotes"
:withReplies="withReplies" :withReplies="withReplies"
:withSensitive="withSensitive"
:onlyFiles="onlyFiles" :onlyFiles="onlyFiles"
@note="onNote" @note="onNote"
/> />
@ -54,6 +55,7 @@ const timeline = shallowRef<InstanceType<typeof MkTimeline>>();
const soundSetting = ref<SoundStore>(props.column.soundSetting ?? { type: null, volume: 1 }); const soundSetting = ref<SoundStore>(props.column.soundSetting ?? { type: null, volume: 1 });
const withRenotes = ref(props.column.withRenotes ?? true); const withRenotes = ref(props.column.withRenotes ?? true);
const withReplies = ref(props.column.withReplies ?? false); const withReplies = ref(props.column.withReplies ?? false);
const withSensitive = ref(props.column.withSensitive ?? true);
const onlyFiles = ref(props.column.onlyFiles ?? false); const onlyFiles = ref(props.column.onlyFiles ?? false);
watch(withRenotes, v => { watch(withRenotes, v => {
@ -68,6 +70,12 @@ watch(withReplies, v => {
}); });
}); });
watch(withSensitive, v => {
updateColumn(props.column.id, {
withSensitive: v,
});
});
watch(onlyFiles, v => { watch(onlyFiles, v => {
updateColumn(props.column.id, { updateColumn(props.column.id, {
onlyFiles: v, onlyFiles: v,
@ -144,6 +152,10 @@ const menu = computed<MenuItem[]>(() => {
text: i18n.ts.fileAttachedOnly, text: i18n.ts.fileAttachedOnly,
ref: onlyFiles, ref: onlyFiles,
disabled: hasWithReplies(props.column.tl) ? withReplies : false, disabled: hasWithReplies(props.column.tl) ? withReplies : false,
}, {
type: 'switch',
text: i18n.ts.withSensitive,
ref: withSensitive,
}); });
return menuItems; return menuItems;

View File

@ -3745,6 +3745,7 @@ export type components = {
}[]; }[];
isBot?: boolean; isBot?: boolean;
isCat?: boolean; isCat?: boolean;
requireSigninToViewContents?: boolean;
instance?: { instance?: {
name: string | null; name: string | null;
softwareName: string | null; softwareName: string | null;
@ -19856,6 +19857,7 @@ export type operations = {
autoAcceptFollowed?: boolean; autoAcceptFollowed?: boolean;
noCrawle?: boolean; noCrawle?: boolean;
preventAiLearning?: boolean; preventAiLearning?: boolean;
requireSigninToViewContents?: boolean;
isBot?: boolean; isBot?: boolean;
isCat?: boolean; isCat?: boolean;
injectFeaturedNote?: boolean; injectFeaturedNote?: boolean;