mirror of
https://codeberg.org/yeentown/barkey.git
synced 2025-07-07 04:26:58 +00:00
merge: Fix regressions and missing parts of recent work (!1102)
View MR for information: https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/1102 Approved-by: dakkar <dakkar@thenautilus.net> Approved-by: Marie <github@yuugi.dev>
This commit is contained in:
commit
cb8ae13685
24 changed files with 349 additions and 203 deletions
12
locales/index.d.ts
vendored
12
locales/index.d.ts
vendored
|
@ -13245,6 +13245,18 @@ export interface Locale extends ILocale {
|
|||
* Note controls
|
||||
*/
|
||||
"noteFooterLabel": string;
|
||||
/**
|
||||
* Packed user data in its raw form. Most of these fields are public and visible to all users.
|
||||
*/
|
||||
"rawUserDescription": string;
|
||||
/**
|
||||
* Extended user data in its raw form. These fields are private and can only be accessed by moderators.
|
||||
*/
|
||||
"rawInfoDescription": string;
|
||||
/**
|
||||
* ActivityPub user data in its raw form. These fields are public and accessible to other instances.
|
||||
*/
|
||||
"rawApDescription": string;
|
||||
}
|
||||
declare const locales: {
|
||||
[lang: string]: Locale;
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class RemoveIDXInstanceHostFilters1749267016885 {
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`DROP INDEX IF EXISTS "IDX_instance_host_filters"`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`
|
||||
create index "IDX_instance_host_filters"
|
||||
on "instance" ("host", "isBlocked", "isSilenced", "isMediaSilenced", "isAllowListed", "isBubbled", "suspensionState")`);
|
||||
await queryRunner.query(`comment on index "IDX_instance_host_filters" is 'Covering index for host filter queries'`);
|
||||
}
|
||||
}
|
|
@ -354,7 +354,7 @@ export class Resolver {
|
|||
|
||||
switch (parsed.type) {
|
||||
case 'notes':
|
||||
return this.notesRepository.findOneByOrFail({ id: parsed.id })
|
||||
return this.notesRepository.findOneByOrFail({ id: parsed.id, userHost: IsNull() })
|
||||
.then(async note => {
|
||||
const author = await this.usersRepository.findOneByOrFail({ id: note.userId });
|
||||
if (parsed.rest === 'activity') {
|
||||
|
@ -365,18 +365,22 @@ export class Resolver {
|
|||
}
|
||||
}) as Promise<IObjectWithId>;
|
||||
case 'users':
|
||||
return this.usersRepository.findOneByOrFail({ id: parsed.id })
|
||||
return this.usersRepository.findOneByOrFail({ id: parsed.id, host: IsNull() })
|
||||
.then(user => this.apRendererService.renderPerson(user as MiLocalUser));
|
||||
case 'questions':
|
||||
// Polls are indexed by the note they are attached to.
|
||||
return Promise.all([
|
||||
this.notesRepository.findOneByOrFail({ id: parsed.id }),
|
||||
this.pollsRepository.findOneByOrFail({ noteId: parsed.id }),
|
||||
this.notesRepository.findOneByOrFail({ id: parsed.id, userHost: IsNull() }),
|
||||
this.pollsRepository.findOneByOrFail({ noteId: parsed.id, userHost: IsNull() }),
|
||||
])
|
||||
.then(([note, poll]) => this.apRendererService.renderQuestion({ id: note.userId }, note, poll)) as Promise<IObjectWithId>;
|
||||
case 'likes':
|
||||
return this.noteReactionsRepository.findOneByOrFail({ id: parsed.id }).then(async reaction =>
|
||||
this.apRendererService.addContext(await this.apRendererService.renderLike(reaction, { uri: null })));
|
||||
return this.noteReactionsRepository.findOneOrFail({ where: { id: parsed.id }, relations: { user: true } }).then(async reaction => {
|
||||
if (reaction.user?.host == null) {
|
||||
throw new IdentifiableError('02b40cd0-fa92-4b0c-acc9-fb2ada952ab8', `failed to resolve local ${url}: not a local reaction`);
|
||||
}
|
||||
return this.apRendererService.addContext(await this.apRendererService.renderLike(reaction, { uri: null }));
|
||||
});
|
||||
case 'follows':
|
||||
return this.followRequestsRepository.findOneBy({ id: parsed.id })
|
||||
.then(async followRequest => {
|
||||
|
|
|
@ -17,6 +17,7 @@ import { DebounceLoader } from '@/misc/loader.js';
|
|||
import { IdService } from '@/core/IdService.js';
|
||||
import { ReactionsBufferingService } from '@/core/ReactionsBufferingService.js';
|
||||
import { isPackedPureRenote } from '@/misc/is-renote.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import type { OnModuleInit } from '@nestjs/common';
|
||||
import type { CacheService } from '../CacheService.js';
|
||||
import type { CustomEmojiService } from '../CustomEmojiService.js';
|
||||
|
@ -92,6 +93,9 @@ export class NoteEntityService implements OnModuleInit {
|
|||
@Inject(DI.channelsRepository)
|
||||
private channelsRepository: ChannelsRepository,
|
||||
|
||||
@Inject(DI.config)
|
||||
private readonly config: Config
|
||||
|
||||
//private userEntityService: UserEntityService,
|
||||
//private driveFileEntityService: DriveFileEntityService,
|
||||
//private customEmojiService: CustomEmojiService,
|
||||
|
@ -680,4 +684,9 @@ export class NoteEntityService implements OnModuleInit {
|
|||
return map;
|
||||
}, {} as Record<string, string | undefined>);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public genLocalNoteUri(noteId: string): string {
|
||||
return `${this.config.url}/notes/${noteId}`;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,7 +7,6 @@ import { Entity, PrimaryColumn, Index, Column } from 'typeorm';
|
|||
import { id } from './util/id.js';
|
||||
|
||||
@Index('IDX_instance_host_key', { synchronize: false }) // ((lower(reverse("host"::text)) || '.'::text)
|
||||
@Index('IDX_instance_host_filters', { synchronize: false }) // ("host", "isBlocked", "isSilenced", "isMediaSilenced", "isAllowListed", "isBubbled", "suspensionState")
|
||||
@Entity('instance')
|
||||
export class MiInstance {
|
||||
@PrimaryColumn(id())
|
||||
|
|
|
@ -3,11 +3,17 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import ms from 'ms';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { ApResolverService } from '@/core/activitypub/ApResolverService.js';
|
||||
import { isCollectionOrOrderedCollection, isOrderedCollection, isOrderedCollectionPage } from '@/core/activitypub/type.js';
|
||||
import { ApiError } from '@/server/api/error.js';
|
||||
import { CacheService } from '@/core/CacheService.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { NotesRepository } from '@/models/_.js';
|
||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['federation'],
|
||||
|
@ -22,6 +28,16 @@ export const meta = {
|
|||
},
|
||||
|
||||
errors: {
|
||||
noInputSpecified: {
|
||||
message: 'uri, userId, or noteId must be specified.',
|
||||
code: 'NO_INPUT_SPECIFIED',
|
||||
id: 'b43ff2a7-e7a2-4237-ad7f-7b079563c09e',
|
||||
},
|
||||
multipleInputsSpecified: {
|
||||
message: 'Only one of uri, userId, or noteId can be specified',
|
||||
code: 'MULTIPLE_INPUTS_SPECIFIED',
|
||||
id: 'f1aa27ed-8f20-44f3-a92a-fe073c8ca52b',
|
||||
},
|
||||
},
|
||||
|
||||
res: {
|
||||
|
@ -33,22 +49,46 @@ export const meta = {
|
|||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
uri: { type: 'string' },
|
||||
uri: { type: 'string', nullable: true },
|
||||
userId: { type: 'string', format: 'misskey:id', nullable: true },
|
||||
noteId: { type: 'string', format: 'misskey:id', nullable: true },
|
||||
expandCollectionItems: { type: 'boolean' },
|
||||
expandCollectionLimit: { type: 'integer', nullable: true },
|
||||
allowAnonymous: { type: 'boolean' },
|
||||
},
|
||||
required: ['uri'],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
@Inject(DI.notesRepository)
|
||||
private readonly notesRepository: NotesRepository,
|
||||
|
||||
private readonly cacheService: CacheService,
|
||||
private readonly userEntityService: UserEntityService,
|
||||
private readonly noteEntityService: NoteEntityService,
|
||||
private apResolverService: ApResolverService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
super(meta, paramDef, async (ps) => {
|
||||
if (ps.uri && ps.userId && ps.noteId) {
|
||||
throw new ApiError(meta.errors.multipleInputsSpecified);
|
||||
}
|
||||
|
||||
let uri: string;
|
||||
if (ps.uri) {
|
||||
uri = ps.uri;
|
||||
} else if (ps.userId) {
|
||||
const user = await this.cacheService.findUserById(ps.userId);
|
||||
uri = user.uri ?? this.userEntityService.genLocalUserUri(ps.userId);
|
||||
} else if (ps.noteId) {
|
||||
const note = await this.notesRepository.findOneByOrFail({ id: ps.noteId });
|
||||
uri = note.uri ?? this.noteEntityService.genLocalNoteUri(ps.noteId);
|
||||
} else {
|
||||
throw new ApiError(meta.errors.noInputSpecified);
|
||||
}
|
||||
|
||||
const resolver = this.apResolverService.createResolver();
|
||||
const object = await resolver.resolve(ps.uri, ps.allowAnonymous ?? false);
|
||||
const object = await resolver.resolve(uri, ps.allowAnonymous ?? false);
|
||||
|
||||
if (ps.expandCollectionItems && isCollectionOrOrderedCollection(object)) {
|
||||
const items = await resolver.resolveCollectionItems(object, ps.expandCollectionLimit, ps.allowAnonymous ?? false);
|
||||
|
|
|
@ -20,9 +20,7 @@ export const meta = {
|
|||
},
|
||||
},
|
||||
|
||||
res: {
|
||||
type: 'object',
|
||||
},
|
||||
res: {},
|
||||
|
||||
// 10 calls per 5 seconds
|
||||
limit: {
|
||||
|
|
|
@ -270,7 +270,7 @@ export const paramDef = {
|
|||
minLength: 1,
|
||||
maxLength: 128,
|
||||
},
|
||||
maxLength: 32,
|
||||
maxItems: 32,
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
|
|
@ -32,6 +32,7 @@ import type { MiLocalUser } from '@/models/User.js';
|
|||
import { getIpHash } from '@/misc/get-ip-hash.js';
|
||||
import { isRetryableError } from '@/misc/is-retryable-error.js';
|
||||
import * as Acct from '@/misc/acct.js';
|
||||
import { isNote } from '@/core/activitypub/type.js';
|
||||
import type { FastifyRequest, FastifyReply } from 'fastify';
|
||||
|
||||
export type LocalSummalyResult = SummalyResult & {
|
||||
|
@ -42,7 +43,7 @@ export type LocalSummalyResult = SummalyResult & {
|
|||
};
|
||||
|
||||
// Increment this to invalidate cached previews after a major change.
|
||||
const cacheFormatVersion = 3;
|
||||
const cacheFormatVersion = 4;
|
||||
|
||||
type PreviewRoute = {
|
||||
Querystring: {
|
||||
|
@ -409,7 +410,7 @@ export class UrlPreviewService {
|
|||
// Finally, attempt a signed GET in case it's a direct link to an instance with authorized fetch.
|
||||
const instanceActor = await this.systemAccountService.getInstanceActor();
|
||||
const remoteObject = await this.apRequestService.signedGet(summary.url, instanceActor).catch(() => null);
|
||||
if (remoteObject && this.apUtilityService.haveSameAuthority(remoteObject.id, summary.url)) {
|
||||
if (remoteObject && isNote(remoteObject) && this.apUtilityService.haveSameAuthority(remoteObject.id, summary.url)) {
|
||||
summary.activityPub = remoteObject.id;
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -95,7 +95,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
<MkPoll v-if="appearNote.poll" :noteId="appearNote.id" :poll="appearNote.poll" :local="!appearNote.user.host" :author="appearNote.user" :emojiUrls="appearNote.emojis" :class="$style.poll" @click.stop/>
|
||||
<div v-if="isEnabledUrlPreview">
|
||||
<MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="false" :showAsQuote="!appearNote.user.rejectQuotes" :skipNoteIds="selfNoteIds" :class="$style.urlPreview" @click.stop/>
|
||||
<SkUrlPreviewGroup :sourceUrls="urls" :sourceNote="appearNote" :compact="true" :detail="false" :showAsQuote="!appearNote.user.rejectQuotes" :skipNoteIds="selfNoteIds" :class="$style.urlPreview" @click.stop/>
|
||||
</div>
|
||||
<div v-if="appearNote.renote" :class="$style.quote"><MkNoteSimple :note="appearNote.renote" :class="$style.quoteNote"/></div>
|
||||
<button v-if="isLong && collapsed" :class="$style.collapsed" class="_button" @click.stop @click="collapsed = false">
|
||||
|
@ -237,6 +237,7 @@ import SkMutedNote from '@/components/SkMutedNote.vue';
|
|||
import SkNoteTranslation from '@/components/SkNoteTranslation.vue';
|
||||
import { getSelfNoteIds } from '@/utility/get-self-note-ids.js';
|
||||
import { extractPreviewUrls } from '@/utility/extract-preview-urls.js';
|
||||
import SkUrlPreviewGroup from '@/components/SkUrlPreviewGroup.vue';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
note: Misskey.entities.Note;
|
||||
|
@ -304,9 +305,9 @@ const galleryEl = useTemplateRef('galleryEl');
|
|||
const isMyRenote = $i && ($i.id === note.value.userId);
|
||||
const showContent = ref(prefer.s.uncollapseCW);
|
||||
const parsed = computed(() => appearNote.value.text ? mfm.parse(appearNote.value.text) : null);
|
||||
const urls = computed(() => parsed.value ? extractPreviewUrls(props.note, parsed.value) : null);
|
||||
const urls = computed(() => parsed.value ? extractPreviewUrls(props.note, parsed.value) : []);
|
||||
const selfNoteIds = computed(() => getSelfNoteIds(props.note));
|
||||
const isLong = shouldCollapsed(appearNote.value, urls.value ?? []);
|
||||
const isLong = shouldCollapsed(appearNote.value, urls.value);
|
||||
const collapsed = ref(prefer.s.expandLongNote && appearNote.value.cw == null && isLong ? false : appearNote.value.cw == null && isLong);
|
||||
const isDeleted = ref(false);
|
||||
const renoted = ref(false);
|
||||
|
|
|
@ -112,7 +112,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
<MkPoll v-if="appearNote.poll" ref="pollViewer" :noteId="appearNote.id" :poll="appearNote.poll" :local="!appearNote.user.host" :class="$style.poll" :author="appearNote.user" :emojiUrls="appearNote.emojis"/>
|
||||
<div v-if="isEnabledUrlPreview">
|
||||
<MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="true" :showAsQuote="!appearNote.user.rejectQuotes" :skipNoteIds="selfNoteIds" style="margin-top: 6px;"/>
|
||||
<SkUrlPreviewGroup :sourceNodes="nodes" :sourceNote="appearNote" :compact="true" :detail="true" :showAsQuote="!appearNote.user.rejectQuotes" :skipNoteIds="selfNoteIds" style="margin-top: 6px;" @click.stop/>
|
||||
</div>
|
||||
<div v-if="appearNote.renote" :class="$style.quote"><MkNoteSimple :note="appearNote.renote" :class="$style.quoteNote" :expandAllCws="props.expandAllCws"/></div>
|
||||
</div>
|
||||
|
@ -286,7 +286,7 @@ import { DI } from '@/di.js';
|
|||
import SkMutedNote from '@/components/SkMutedNote.vue';
|
||||
import SkNoteTranslation from '@/components/SkNoteTranslation.vue';
|
||||
import { getSelfNoteIds } from '@/utility/get-self-note-ids.js';
|
||||
import { extractPreviewUrls } from '@/utility/extract-preview-urls.js';
|
||||
import SkUrlPreviewGroup from '@/components/SkUrlPreviewGroup.vue';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
note: Misskey.entities.Note;
|
||||
|
@ -339,8 +339,7 @@ const isDeleted = ref(false);
|
|||
const renoted = ref(false);
|
||||
const translation = ref<Misskey.entities.NotesTranslateResponse | false | null>(null);
|
||||
const translating = ref(false);
|
||||
const parsed = computed(() => appearNote.value.text ? mfm.parse(appearNote.value.text) : null);
|
||||
const urls = computed(() => parsed.value ? extractPreviewUrls(props.note, parsed.value) : null);
|
||||
const parsed = computed(() => appearNote.value.text ? mfm.parse(appearNote.value.text) : []);
|
||||
const selfNoteIds = computed(() => getSelfNoteIds(props.note));
|
||||
const animated = computed(() => parsed.value ? checkAnimationFromMfm(parsed.value) : null);
|
||||
const allowAnim = ref(prefer.s.advancedMfm && prefer.s.animatedMfm);
|
||||
|
|
|
@ -97,7 +97,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
<MkPoll v-if="appearNote.poll" :noteId="appearNote.id" :poll="appearNote.poll" :local="!appearNote.user.host" :author="appearNote.user" :emojiUrls="appearNote.emojis" :class="$style.poll" @click.stop/>
|
||||
<div v-if="isEnabledUrlPreview">
|
||||
<MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="false" :showAsQuote="!appearNote.user.rejectQuotes" :skipNoteIds="selfNoteIds" :class="$style.urlPreview" @click.stop/>
|
||||
<SkUrlPreviewGroup :sourceUrls="urls" :sourceNote="appearNote" :compact="true" :detail="false" :showAsQuote="!appearNote.user.rejectQuotes" :skipNoteIds="selfNoteIds" :class="$style.urlPreview" @click.stop/>
|
||||
</div>
|
||||
<div v-if="appearNote.renote" :class="$style.quote"><SkNoteSimple :note="appearNote.renote" :class="$style.quoteNote"/></div>
|
||||
<button v-if="isLong && collapsed" :class="$style.collapsed" class="_button" @click.stop @click="collapsed = false">
|
||||
|
@ -237,6 +237,7 @@ import SkMutedNote from '@/components/SkMutedNote.vue';
|
|||
import SkNoteTranslation from '@/components/SkNoteTranslation.vue';
|
||||
import { getSelfNoteIds } from '@/utility/get-self-note-ids.js';
|
||||
import { extractPreviewUrls } from '@/utility/extract-preview-urls.js';
|
||||
import SkUrlPreviewGroup from '@/components/SkUrlPreviewGroup.vue';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
note: Misskey.entities.Note;
|
||||
|
@ -304,9 +305,9 @@ const galleryEl = useTemplateRef('galleryEl');
|
|||
const isMyRenote = $i && ($i.id === note.value.userId);
|
||||
const showContent = ref(prefer.s.uncollapseCW);
|
||||
const parsed = computed(() => appearNote.value.text ? mfm.parse(appearNote.value.text) : null);
|
||||
const urls = computed(() => parsed.value ? extractPreviewUrls(props.note, parsed.value) : null);
|
||||
const urls = computed(() => parsed.value ? extractPreviewUrls(props.note, parsed.value) : []);
|
||||
const selfNoteIds = computed(() => getSelfNoteIds(props.note));
|
||||
const isLong = shouldCollapsed(appearNote.value, urls.value ?? []);
|
||||
const isLong = shouldCollapsed(appearNote.value, urls.value);
|
||||
const collapsed = ref(prefer.s.expandLongNote && appearNote.value.cw == null && isLong ? false : appearNote.value.cw == null && isLong);
|
||||
const isDeleted = ref(false);
|
||||
const renoted = ref(false);
|
||||
|
|
|
@ -117,7 +117,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
<MkPoll v-if="appearNote.poll" ref="pollViewer" :noteId="appearNote.id" :poll="appearNote.poll" :local="!appearNote.user.host" :class="$style.poll" :author="appearNote.user" :emojiUrls="appearNote.emojis"/>
|
||||
<div v-if="isEnabledUrlPreview">
|
||||
<MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="true" :showAsQuote="!appearNote.user.rejectQuotes" :skipNoteIds="selfNoteIds" style="margin-top: 6px;"/>
|
||||
<SkUrlPreviewGroup :sourceNodes="nodes" :sourceNote="appearNote" :compact="true" :detail="true" :showAsQuote="!appearNote.user.rejectQuotes" :skipNoteIds="selfNoteIds" style="margin-top: 6px;" @click.stop/>
|
||||
</div>
|
||||
<div v-if="appearNote.renote" :class="$style.quote"><SkNoteSimple :note="appearNote.renote" :class="$style.quoteNote" :expandAllCws="props.expandAllCws"/></div>
|
||||
</div>
|
||||
|
@ -291,7 +291,7 @@ import { DI } from '@/di.js';
|
|||
import SkMutedNote from '@/components/SkMutedNote.vue';
|
||||
import SkNoteTranslation from '@/components/SkNoteTranslation.vue';
|
||||
import { getSelfNoteIds } from '@/utility/get-self-note-ids.js';
|
||||
import { extractPreviewUrls } from '@/utility/extract-preview-urls';
|
||||
import SkUrlPreviewGroup from '@/components/SkUrlPreviewGroup.vue';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
note: Misskey.entities.Note;
|
||||
|
@ -345,8 +345,7 @@ const isDeleted = ref(false);
|
|||
const renoted = ref(false);
|
||||
const translation = ref<Misskey.entities.NotesTranslateResponse | false | null>(null);
|
||||
const translating = ref(false);
|
||||
const parsed = computed(() => appearNote.value.text ? mfm.parse(appearNote.value.text) : null);
|
||||
const urls = computed(() => parsed.value ? extractPreviewUrls(props.note, parsed.value) : null);
|
||||
const parsed = computed(() => appearNote.value.text ? mfm.parse(appearNote.value.text) : []);
|
||||
const selfNoteIds = computed(() => getSelfNoteIds(props.note));
|
||||
const animated = computed(() => parsed.value ? checkAnimationFromMfm(parsed.value) : null);
|
||||
const allowAnim = ref(prefer.s.advancedMfm && prefer.s.animatedMfm ? true : false);
|
||||
|
|
|
@ -40,14 +40,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div v-show="appearNote.cw == null || showContent">
|
||||
<span v-if="appearNote.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span>
|
||||
<MkA v-if="appearNote.replyId" :class="$style.noteReplyTarget" :to="`/notes/${appearNote.replyId}`"><i class="ph-arrow-bend-left-up ph-bold ph-lg"></i></MkA>
|
||||
<Mfm v-if="appearNote.text" :text="appearNote.text" :isBlock="true" :author="appearNote.user" :nyaize="'respect'" :emojiUrls="appearNote.emojis"/>
|
||||
<Mfm v-if="appearNote.text" :text="appearNote.text" :parsedNodes="parsed" :isBlock="true" :author="appearNote.user" :nyaize="'respect'" :emojiUrls="appearNote.emojis"/>
|
||||
<a v-if="appearNote.renote != null" :class="$style.rn">RN:</a>
|
||||
<SkNoteTranslation :note="note" :translation="translation" :translating="translating"></SkNoteTranslation>
|
||||
<div v-if="appearNote.files && appearNote.files.length > 0">
|
||||
<MkMediaList :mediaList="appearNote.files"/>
|
||||
</div>
|
||||
<MkPoll v-if="appearNote.poll" :noteId="appearNote.id" :poll="appearNote.poll" :local="!appearNote.user.host" :author="appearNote.user" :emojiUrls="appearNote.emojis" :class="$style.poll"/>
|
||||
<MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="true" :showAsQuote="!appearNote.user.rejectQuotes" :skipNoteIds="selfNoteIds" style="margin-top: 6px;"/>
|
||||
<SkUrlPreviewGroup :sourceNodes="parsed" :sourceNote="appearNote" :compact="true" :detail="true" :showAsQuote="!appearNote.user.rejectQuotes" :skipNoteIds="selfNoteIds" style="margin-top: 6px;" @click.stop/>
|
||||
<div v-if="appearNote.renote" :class="$style.quote"><MkNoteSimple :note="appearNote.renote" :class="$style.quoteNote"/></div>
|
||||
</div>
|
||||
<MkA v-if="appearNote.channel && !inChannel" :class="$style.channel" :to="`/channels/${appearNote.channel.id}`"><i class="ph-television ph-bold ph-lg"></i> {{ appearNote.channel.name }}</MkA>
|
||||
|
@ -83,7 +83,6 @@ import MkMediaList from '@/components/MkMediaList.vue';
|
|||
import MkCwButton from '@/components/MkCwButton.vue';
|
||||
import MkWindow from '@/components/MkWindow.vue';
|
||||
import MkPoll from '@/components/MkPoll.vue';
|
||||
import MkUrlPreview from '@/components/MkUrlPreview.vue';
|
||||
import MkInstanceTicker from '@/components/MkInstanceTicker.vue';
|
||||
import { userPage } from '@/filters/user.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
@ -93,7 +92,7 @@ import { prefer } from '@/preferences';
|
|||
import { getPluginHandlers } from '@/plugin.js';
|
||||
import SkNoteTranslation from '@/components/SkNoteTranslation.vue';
|
||||
import { getSelfNoteIds } from '@/utility/get-self-note-ids';
|
||||
import { extractPreviewUrls } from '@/utility/extract-preview-urls.js';
|
||||
import SkUrlPreviewGroup from '@/components/SkUrlPreviewGroup.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
note: Misskey.entities.Note;
|
||||
|
@ -143,12 +142,11 @@ const isRenote = (
|
|||
|
||||
const el = shallowRef<HTMLElement>();
|
||||
const appearNote = computed(() => isRenote ? note.value.renote as Misskey.entities.Note : note.value);
|
||||
const parsed = computed(() => appearNote.value.text ? mfm.parse(appearNote.value.text) : null);
|
||||
const parsed = computed(() => appearNote.value.text ? mfm.parse(appearNote.value.text) : []);
|
||||
|
||||
const showContent = ref(false);
|
||||
const translation = ref<Misskey.entities.NotesTranslateResponse | false | null>(null);
|
||||
const translating = ref(false);
|
||||
const urls = computed(() => parsed.value ? extractPreviewUrls(props.note, parsed.value) : null);
|
||||
const selfNoteIds = computed(() => getSelfNoteIds(props.note));
|
||||
const showTicker = (prefer.s.instanceTicker === 'always') || (prefer.s.instanceTicker === 'remote' && appearNote.value.user.instance);
|
||||
|
||||
|
|
|
@ -7,29 +7,20 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div class="_gaps" :class="$style.textRoot">
|
||||
<Mfm :text="block.text ?? ''" :isBlock="true" :isNote="false"/>
|
||||
<div v-if="isEnabledUrlPreview" class="_gaps_s">
|
||||
<MkUrlPreview v-for="url in urls" :key="url" :url="url" :showAsQuote="!page.user.rejectQuotes"/>
|
||||
<SkUrlPreviewGroup :sourceText="block.text" :showAsQuote="!page.user.rejectQuotes" @click.stop/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { defineAsyncComponent, computed } from 'vue';
|
||||
import * as mfm from '@transfem-org/sfm-js';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { extractUrlFromMfm } from '@/utility/extract-url-from-mfm.js';
|
||||
import { isEnabledUrlPreview } from '@/instance.js';
|
||||
import SkUrlPreviewGroup from '@/components/SkUrlPreviewGroup.vue';
|
||||
|
||||
const MkUrlPreview = defineAsyncComponent(() => import('@/components/MkUrlPreview.vue'));
|
||||
|
||||
const props = defineProps<{
|
||||
defineProps<{
|
||||
block: Misskey.entities.PageBlock,
|
||||
page: Misskey.entities.Page,
|
||||
}>();
|
||||
|
||||
const urls = computed(() => {
|
||||
if (!props.block.text) return [];
|
||||
return extractUrlFromMfm(mfm.parse(props.block.text));
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
|
|
@ -5,10 +5,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<template>
|
||||
<PageWithHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs" :spacer="true" style="--MI_SPACER-w: 600px; --MI_SPACER-min: 16px; --MI_SPACER-max: 32px;">
|
||||
<div>
|
||||
<FormSuspense :p="init">
|
||||
<FormSuspense v-if="init" :p="init">
|
||||
<div v-if="user && info">
|
||||
<div v-if="tab === 'overview'" class="_gaps">
|
||||
<div v-if="user" class="aeakzknw">
|
||||
<div class="aeakzknw">
|
||||
<MkAvatar class="avatar" :user="user" indicator link preview/>
|
||||
<div class="body">
|
||||
<span class="name"><MkUserName class="name" :user="user"/></span>
|
||||
|
@ -228,14 +228,46 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
|
||||
<div v-else-if="tab === 'raw'" class="_gaps_m">
|
||||
<MkObjectView v-if="info && $i.isAdmin" tall :value="info">
|
||||
</MkObjectView>
|
||||
<MkFolder :sticky="false" :defaultOpen="true">
|
||||
<template #icon><i class="ph-user-circle ph-bold ph-lg"></i></template>
|
||||
<template #label>{{ i18n.ts.user }}</template>
|
||||
<template #header>
|
||||
<div :class="$style.rawFolderHeader">
|
||||
<span>{{ i18n.ts.rawUserDescription }}</span>
|
||||
<button class="_textButton" @click="copyToClipboard(JSON.stringify(user, null, 4))"><i class="ti ti-copy"></i> {{ i18n.ts.copy }}</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<MkObjectView tall :value="user">
|
||||
</MkObjectView>
|
||||
<MkObjectView tall :value="user"/>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder :sticky="false">
|
||||
<template #icon><i class="ti ti-info-circle"></i></template>
|
||||
<template #label>{{ i18n.ts.details }}</template>
|
||||
<template #header>
|
||||
<div :class="$style.rawFolderHeader">
|
||||
<span>{{ i18n.ts.rawInfoDescription }}</span>
|
||||
<button class="_textButton" @click="copyToClipboard(JSON.stringify(info, null, 4))"><i class="ti ti-copy"></i> {{ i18n.ts.copy }}</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<MkObjectView tall :value="info"/>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder v-if="ap" :sticky="false">
|
||||
<template #icon><i class="ph-globe ph-bold ph-lg"></i></template>
|
||||
<template #label>{{ i18n.ts.activityPub }}</template>
|
||||
<template #header>
|
||||
<div :class="$style.rawFolderHeader">
|
||||
<span>{{ i18n.ts.rawApDescription }}</span>
|
||||
<button class="_textButton" @click="copyToClipboard(JSON.stringify(ap, null, 4))"><i class="ti ti-copy"></i> {{ i18n.ts.copy }}</button>
|
||||
</div>
|
||||
</template>
|
||||
<MkObjectView tall :value="ap"/>
|
||||
</MkFolder>
|
||||
</div>
|
||||
</div>
|
||||
</FormSuspense>
|
||||
</div>
|
||||
</PageWithHeader>
|
||||
</template>
|
||||
|
||||
|
@ -244,6 +276,7 @@ import { computed, defineAsyncComponent, watch, ref } from 'vue';
|
|||
import * as Misskey from 'misskey-js';
|
||||
import { url } from '@@/js/config.js';
|
||||
import type { Badge } from '@/components/SkBadgeStrip.vue';
|
||||
import type { ChartSrc } from '@/components/MkChart.vue';
|
||||
import MkChart from '@/components/MkChart.vue';
|
||||
import MkObjectView from '@/components/MkObjectView.vue';
|
||||
import MkTextarea from '@/components/MkTextarea.vue';
|
||||
|
@ -276,15 +309,17 @@ const props = withDefaults(defineProps<{
|
|||
userHint?: Misskey.entities.UserDetailed;
|
||||
infoHint?: Misskey.entities.AdminShowUserResponse;
|
||||
ipsHint?: Misskey.entities.AdminGetUserIpsResponse;
|
||||
apHint?: Misskey.entities.ApGetResponse;
|
||||
}>(), {
|
||||
initialTab: 'overview',
|
||||
userHint: undefined,
|
||||
infoHint: undefined,
|
||||
ipsHint: undefined,
|
||||
apHint: undefined,
|
||||
});
|
||||
|
||||
const tab = ref(props.initialTab);
|
||||
const chartSrc = ref('per-user-notes');
|
||||
const chartSrc = ref<ChartSrc>('per-user-notes');
|
||||
const user = ref<null | Misskey.entities.UserDetailed>();
|
||||
const init = ref<ReturnType<typeof createFetcher>>();
|
||||
const info = ref<Misskey.entities.AdminShowUserResponse | null>(null);
|
||||
|
@ -409,7 +444,7 @@ const announcementsPagination = {
|
|||
status: announcementsStatus.value,
|
||||
})),
|
||||
};
|
||||
const expandedRoles = ref([]);
|
||||
const expandedRoles = ref<string[]>([]);
|
||||
|
||||
function createFetcher(withHint = true) {
|
||||
return () => Promise.all([
|
||||
|
@ -424,22 +459,23 @@ function createFetcher(withHint = true) {
|
|||
userId: props.userId,
|
||||
})
|
||||
: null,
|
||||
iAmAdmin ? misskeyApi('ap/get', {
|
||||
uri: `${url}/users/${props.userId}`,
|
||||
iAmAdmin
|
||||
? (withHint && props.apHint) ? props.apHint : misskeyApi('ap/get', {
|
||||
userId: props.userId,
|
||||
}).catch(() => null) : null],
|
||||
).then(([_user, _info, _ips, _ap]) => {
|
||||
).then(async ([_user, _info, _ips, _ap]) => {
|
||||
user.value = _user;
|
||||
info.value = _info;
|
||||
ips.value = _ips;
|
||||
ap.value = _ap;
|
||||
moderator.value = info.value.isModerator;
|
||||
silenced.value = info.value.isSilenced;
|
||||
approved.value = info.value.approved;
|
||||
markedAsNSFW.value = info.value.alwaysMarkNsfw;
|
||||
suspended.value = info.value.isSuspended;
|
||||
rejectQuotes.value = user.value.rejectQuotes ?? false;
|
||||
moderationNote.value = info.value.moderationNote;
|
||||
mandatoryCW.value = user.value.mandatoryCW;
|
||||
moderator.value = _info.isModerator;
|
||||
silenced.value = _info.isSilenced;
|
||||
approved.value = _info.approved;
|
||||
markedAsNSFW.value = _info.alwaysMarkNsfw;
|
||||
suspended.value = _info.isSuspended;
|
||||
rejectQuotes.value = _user.rejectQuotes ?? false;
|
||||
moderationNote.value = _info.moderationNote;
|
||||
mandatoryCW.value = _user.mandatoryCW;
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -448,9 +484,9 @@ async function refreshUser() {
|
|||
await createFetcher(false)();
|
||||
}
|
||||
|
||||
async function onMandatoryCWChanged(value: string) {
|
||||
async function onMandatoryCWChanged(value: string | number) {
|
||||
await os.promiseDialog(async () => {
|
||||
await misskeyApi('admin/cw-user', { userId: props.userId, cw: value });
|
||||
await misskeyApi('admin/cw-user', { userId: props.userId, cw: String(value) });
|
||||
await refreshUser();
|
||||
});
|
||||
}
|
||||
|
@ -458,14 +494,14 @@ async function onMandatoryCWChanged(value: string) {
|
|||
async function onModerationNoteChanged(value: string) {
|
||||
await os.promiseDialog(async () => {
|
||||
await misskeyApi('admin/update-user-note', { userId: props.userId, text: value });
|
||||
refreshUser();
|
||||
await refreshUser();
|
||||
});
|
||||
}
|
||||
|
||||
async function updateRemoteUser() {
|
||||
await os.promiseDialog(async () => {
|
||||
await misskeyApi('federation/update-remote-user', { userId: props.userId });
|
||||
refreshUser();
|
||||
await refreshUser();
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -478,7 +514,7 @@ async function resetPassword() {
|
|||
return;
|
||||
} else {
|
||||
const { password } = await misskeyApi('admin/reset-password', {
|
||||
userId: user.value.id,
|
||||
userId: props.userId,
|
||||
});
|
||||
await os.alert({
|
||||
type: 'success',
|
||||
|
@ -590,15 +626,16 @@ async function deleteAccount() {
|
|||
text: i18n.ts.deleteThisAccountConfirm,
|
||||
});
|
||||
if (confirm.canceled) return;
|
||||
if (!user.value) return;
|
||||
|
||||
const typed = await os.inputText({
|
||||
text: i18n.tsx.typeToConfirm({ x: user.value?.username }),
|
||||
text: i18n.tsx.typeToConfirm({ x: user.value.username }),
|
||||
});
|
||||
if (typed.canceled) return;
|
||||
|
||||
if (typed.result === user.value?.username) {
|
||||
if (typed.result === user.value.username) {
|
||||
await os.apiWithDialog('admin/delete-account', {
|
||||
userId: user.value.id,
|
||||
userId: props.userId,
|
||||
});
|
||||
} else {
|
||||
await os.alert({
|
||||
|
@ -661,7 +698,7 @@ async function unassignRole(role, ev) {
|
|||
}], ev.currentTarget ?? ev.target);
|
||||
}
|
||||
|
||||
function toggleRoleItem(role) {
|
||||
function toggleRoleItem(role: Misskey.entities.Role) {
|
||||
if (expandedRoles.value.includes(role.id)) {
|
||||
expandedRoles.value = expandedRoles.value.filter(x => x !== role.id);
|
||||
} else {
|
||||
|
@ -670,6 +707,7 @@ function toggleRoleItem(role) {
|
|||
}
|
||||
|
||||
function createAnnouncement() {
|
||||
if (!user.value) return;
|
||||
const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkUserAnnouncementEditDialog.vue')), {
|
||||
user: user.value,
|
||||
}, {
|
||||
|
@ -678,6 +716,7 @@ function createAnnouncement() {
|
|||
}
|
||||
|
||||
function editAnnouncement(announcement) {
|
||||
if (!user.value) return;
|
||||
const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkUserAnnouncementEditDialog.vue')), {
|
||||
user: user.value,
|
||||
announcement,
|
||||
|
@ -883,4 +922,13 @@ definePage(() => ({
|
|||
margin: calc(var(--MI-margin) / 2);
|
||||
}
|
||||
}
|
||||
|
||||
.rawFolderHeader {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
padding: var(--MI-marginHalf);
|
||||
gap: var(--MI-marginHalf);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -14,6 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
ref="text"
|
||||
class="_selectable"
|
||||
:text="message.text"
|
||||
:parsedNotes="parsed"
|
||||
:i="$i"
|
||||
:nyaize="'respect'"
|
||||
:enableEmojiMenu="true"
|
||||
|
@ -21,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
/>
|
||||
<MkMediaList v-if="message.file" :mediaList="[message.file]" :class="$style.file"/>
|
||||
</MkFukidashi>
|
||||
<MkUrlPreview v-for="url in urls" :key="url" :url="url" :showAsQuote="!message.fromUser.rejectQuotes" style="margin: 8px 0;"/>
|
||||
<SkUrlPreviewGroup :sourceNodes="parsed" :showAsQuote="!message.fromUser.rejectQuotes" style="margin: 8px 0;"/>
|
||||
<div :class="$style.footer">
|
||||
<button class="_textButton" style="color: currentColor;" @click="showMenu"><i class="ti ti-dots-circle-horizontal"></i></button>
|
||||
<MkTime :class="$style.time" :time="message.createdAt"/>
|
||||
|
@ -58,8 +59,6 @@ import { url } from '@@/js/config.js';
|
|||
import { isLink } from '@@/js/is-link.js';
|
||||
import type { MenuItem } from '@/types/menu.js';
|
||||
import type { NormalizedChatMessage } from './room.vue';
|
||||
import { extractUrlFromMfm } from '@/utility/extract-url-from-mfm.js';
|
||||
import MkUrlPreview from '@/components/MkUrlPreview.vue';
|
||||
import { ensureSignin } from '@/i.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
@ -74,6 +73,7 @@ import { prefer } from '@/preferences.js';
|
|||
import { DI } from '@/di.js';
|
||||
import { getHTMLElementOrNull } from '@/utility/get-dom-node-or-null.js';
|
||||
import SkTransitionGroup from '@/components/SkTransitionGroup.vue';
|
||||
import SkUrlPreviewGroup from '@/components/SkUrlPreviewGroup.vue';
|
||||
|
||||
const $i = ensureSignin();
|
||||
|
||||
|
@ -83,7 +83,7 @@ const props = defineProps<{
|
|||
}>();
|
||||
|
||||
const isMe = computed(() => props.message.fromUserId === $i.id);
|
||||
const urls = computed(() => props.message.text ? extractUrlFromMfm(mfm.parse(props.message.text)) : []);
|
||||
const parsed = computed(() => props.message.text ? mfm.parse(props.message.text) : []);
|
||||
|
||||
provide(DI.mfmEmojiReactCallback, (reaction) => {
|
||||
if ($i.policies.chatAvailability !== 'available') return;
|
||||
|
|
|
@ -23,24 +23,8 @@ import { i18n } from '@/i18n.js';
|
|||
import { definePage } from '@/page.js';
|
||||
import { miLocalStorage } from '@/local-storage.js';
|
||||
import { prefer } from '@/preferences.js';
|
||||
import { reloadAsk } from '@/utility/reload-ask';
|
||||
|
||||
const customCssModel = prefer.model('customCss');
|
||||
const localCustomCss = computed<string>({
|
||||
get() {
|
||||
return customCssModel.value ?? miLocalStorage.getItem('customCss') ?? '';
|
||||
},
|
||||
set(newCustomCss) {
|
||||
customCssModel.value = newCustomCss;
|
||||
if (newCustomCss) {
|
||||
miLocalStorage.setItem('customCss', newCustomCss);
|
||||
} else {
|
||||
miLocalStorage.removeItem('customCss');
|
||||
}
|
||||
|
||||
reloadAsk(true);
|
||||
},
|
||||
});
|
||||
const localCustomCss = prefer.model('customCss');
|
||||
|
||||
const headerActions = computed(() => []);
|
||||
|
||||
|
|
|
@ -1059,68 +1059,13 @@ const clickToOpen = prefer.model('clickToOpen');
|
|||
const useCustomSearchEngine = computed(() => !Object.keys(searchEngineMap).includes(searchEngine.value));
|
||||
const defaultCW = ref($i.defaultCW);
|
||||
const defaultCWPriority = ref($i.defaultCWPriority);
|
||||
|
||||
const langModel = prefer.model('lang');
|
||||
const lang = computed<string>({
|
||||
get() {
|
||||
return langModel.value ?? miLocalStorage.getItem('lang') ?? 'en-US';
|
||||
},
|
||||
set(newLang) {
|
||||
langModel.value = newLang;
|
||||
miLocalStorage.setItem('lang', newLang);
|
||||
miLocalStorage.removeItem('locale');
|
||||
miLocalStorage.removeItem('localeVersion');
|
||||
},
|
||||
});
|
||||
|
||||
const fontSizeModel = prefer.model('fontSize');
|
||||
const fontSize = computed<'0' | '1' | '2' | '3'>({
|
||||
get() {
|
||||
return fontSizeModel.value ?? miLocalStorage.getItem('fontSize') ?? '0';
|
||||
},
|
||||
set(newFontSize) {
|
||||
fontSizeModel.value = newFontSize;
|
||||
if (newFontSize !== '0') {
|
||||
miLocalStorage.setItem('fontSize', newFontSize);
|
||||
} else {
|
||||
miLocalStorage.removeItem('fontSize');
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const useSystemFontModel = prefer.model('useSystemFont');
|
||||
const useSystemFont = computed<boolean>({
|
||||
get() {
|
||||
return useSystemFontModel.value ?? (miLocalStorage.getItem('useSystemFont') != null);
|
||||
},
|
||||
set(newUseSystemFont) {
|
||||
useSystemFontModel.value = newUseSystemFont;
|
||||
if (newUseSystemFont) {
|
||||
miLocalStorage.setItem('useSystemFont', 't');
|
||||
} else {
|
||||
miLocalStorage.removeItem('useSystemFont');
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const cornerRadiusModel = prefer.model('cornerRadius');
|
||||
const cornerRadius = computed<'misskey' | 'sharkey'>({
|
||||
get() {
|
||||
return cornerRadiusModel.value ?? miLocalStorage.getItem('cornerRadius') ?? 'sharkey';
|
||||
},
|
||||
set(newCornerRadius) {
|
||||
cornerRadiusModel.value = newCornerRadius;
|
||||
if (newCornerRadius === 'sharkey') {
|
||||
miLocalStorage.removeItem('cornerRadius');
|
||||
} else {
|
||||
miLocalStorage.setItem('cornerRadius', newCornerRadius);
|
||||
}
|
||||
},
|
||||
});
|
||||
const lang = prefer.model('lang');
|
||||
const fontSize = prefer.model('fontSize');
|
||||
const useSystemFont = prefer.model('useSystemFont');
|
||||
const cornerRadius = prefer.model('cornerRadius');
|
||||
|
||||
watch([
|
||||
hemisphere,
|
||||
lang,
|
||||
enableInfiniteScroll,
|
||||
showNoteActionsOnlyHover,
|
||||
overridedDeviceKind,
|
||||
|
@ -1142,9 +1087,6 @@ watch([
|
|||
useStickyIcons,
|
||||
keepScreenOn,
|
||||
contextMenu,
|
||||
fontSize,
|
||||
useSystemFont,
|
||||
cornerRadius,
|
||||
makeEveryTextElementsSelectable,
|
||||
noteDesign,
|
||||
], async () => {
|
||||
|
|
|
@ -13,15 +13,18 @@ import { deckStore } from '@/ui/deck/deck-store.js';
|
|||
import { unisonReload } from '@/utility/unison-reload.js';
|
||||
import * as os from '@/os.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { miLocalStorage } from '@/local-storage';
|
||||
|
||||
// TODO: そのうち消す
|
||||
export function migrateOldSettings() {
|
||||
os.waiting(i18n.ts.settingsMigrating);
|
||||
|
||||
store.loaded.then(async () => {
|
||||
misskeyApi('i/registry/get', { scope: ['client'], key: 'themes' }).catch(() => []).then((themes: Theme[]) => {
|
||||
if (themes.length > 0) {
|
||||
prefer.commit('themes', themes);
|
||||
prefer.suppressReload();
|
||||
|
||||
await misskeyApi('i/registry/get', { scope: ['client'], key: 'themes' }).catch(() => []).then(themes => {
|
||||
if (Array.isArray(themes) && themes.length > 0) {
|
||||
prefer.commit('themes', themes as Theme[]);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -33,7 +36,7 @@ export function migrateOldSettings() {
|
|||
})));
|
||||
|
||||
prefer.commit('deck.profile', deckStore.s.profile);
|
||||
misskeyApi('i/registry/keys', {
|
||||
await misskeyApi('i/registry/keys', {
|
||||
scope: ['client', 'deck', 'profiles'],
|
||||
}).then(async keys => {
|
||||
const profiles: DeckProfile[] = [];
|
||||
|
@ -41,16 +44,18 @@ export function migrateOldSettings() {
|
|||
const deck = await misskeyApi('i/registry/get', {
|
||||
scope: ['client', 'deck', 'profiles'],
|
||||
key: key,
|
||||
});
|
||||
}).catch(() => null);
|
||||
if (deck) {
|
||||
profiles.push({
|
||||
id: uuid(),
|
||||
name: key,
|
||||
columns: deck.columns,
|
||||
layout: deck.layout,
|
||||
columns: (deck as DeckProfile).columns,
|
||||
layout: (deck as DeckProfile).layout,
|
||||
});
|
||||
}
|
||||
}
|
||||
prefer.commit('deck.profiles', profiles);
|
||||
});
|
||||
}).catch(() => null);
|
||||
|
||||
prefer.commit('lightTheme', ColdDeviceStorage.get('lightTheme'));
|
||||
prefer.commit('darkTheme', ColdDeviceStorage.get('darkTheme'));
|
||||
|
@ -164,8 +169,17 @@ export function migrateOldSettings() {
|
|||
prefer.commit('warnMissingAltText', store.s.warnMissingAltText);
|
||||
//#endregion
|
||||
|
||||
window.setTimeout(() => {
|
||||
//#region Hybrid migrations
|
||||
prefer.commit('fontSize', miLocalStorage.getItem('fontSize') ?? '0');
|
||||
prefer.commit('useSystemFont', miLocalStorage.getItem('useSystemFont') != null);
|
||||
prefer.commit('cornerRadius', miLocalStorage.getItem('cornerRadius') ?? 'sharkey');
|
||||
prefer.commit('lang', miLocalStorage.getItem('lang') ?? 'en-US');
|
||||
prefer.commit('customCss', miLocalStorage.getItem('customCss') ?? '');
|
||||
prefer.commit('neverShowDonationInfo', miLocalStorage.getItem('neverShowDonationInfo') != null);
|
||||
prefer.commit('neverShowLocalOnlyInfo', miLocalStorage.getItem('neverShowLocalOnlyInfo') != null);
|
||||
//#endregion
|
||||
|
||||
prefer.allowReload();
|
||||
unisonReload();
|
||||
}, 10000);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -10,11 +10,12 @@ import type { SoundType } from '@/utility/sound.js';
|
|||
import type { Plugin } from '@/plugin.js';
|
||||
import type { DeviceKind } from '@/utility/device-kind.js';
|
||||
import type { DeckProfile } from '@/deck.js';
|
||||
import type { PreferencesDefinition } from './manager.js';
|
||||
import type { Pref, PreferencesDefinition } from './manager.js';
|
||||
import type { FollowingFeedState } from '@/types/following-feed.js';
|
||||
import { DEFAULT_DEVICE_KIND } from '@/utility/device-kind.js';
|
||||
import { searchEngineMap } from '@/utility/search-engine-map.js';
|
||||
import { defaultFollowingFeedState } from '@/types/following-feed.js';
|
||||
import { miLocalStorage } from '@/local-storage';
|
||||
|
||||
/** サウンド設定 */
|
||||
export type SoundStore = {
|
||||
|
@ -484,25 +485,77 @@ export const PREF_DEF = {
|
|||
// Null means "fall back to existing value from localStorage"
|
||||
// For all of these preferences, "null" means fall back to existing value in localStorage.
|
||||
fontSize: {
|
||||
default: null as null | '0' | '1' | '2' | '3',
|
||||
default: '0',
|
||||
needsReload: true,
|
||||
onSet: fontSize => {
|
||||
if (fontSize !== '0') {
|
||||
miLocalStorage.setItem('fontSize', fontSize);
|
||||
} else {
|
||||
miLocalStorage.removeItem('fontSize');
|
||||
}
|
||||
},
|
||||
} as Pref<'0' | '1' | '2' | '3'>,
|
||||
useSystemFont: {
|
||||
default: null as null | boolean,
|
||||
default: false,
|
||||
needsReload: true,
|
||||
onSet: useSystemFont => {
|
||||
if (useSystemFont) {
|
||||
miLocalStorage.setItem('useSystemFont', 't');
|
||||
} else {
|
||||
miLocalStorage.removeItem('useSystemFont');
|
||||
}
|
||||
},
|
||||
} as Pref<boolean>,
|
||||
cornerRadius: {
|
||||
default: null as null | 'misskey' | 'sharkey',
|
||||
default: 'sharkey',
|
||||
needsReload: true,
|
||||
onSet: cornerRadius => {
|
||||
if (cornerRadius === 'sharkey') {
|
||||
miLocalStorage.removeItem('cornerRadius');
|
||||
} else {
|
||||
miLocalStorage.setItem('cornerRadius', cornerRadius);
|
||||
}
|
||||
},
|
||||
} as Pref<'misskey' | 'sharkey'>,
|
||||
lang: {
|
||||
default: null as null | string,
|
||||
default: 'en-US',
|
||||
needsReload: true,
|
||||
onSet: lang => {
|
||||
miLocalStorage.setItem('lang', lang);
|
||||
miLocalStorage.removeItem('locale');
|
||||
miLocalStorage.removeItem('localeVersion');
|
||||
},
|
||||
} as Pref<string>,
|
||||
customCss: {
|
||||
default: null as null | string,
|
||||
default: '',
|
||||
needsReload: true,
|
||||
onSet: customCss => {
|
||||
if (customCss) {
|
||||
miLocalStorage.setItem('customCss', customCss);
|
||||
} else {
|
||||
miLocalStorage.removeItem('customCss');
|
||||
}
|
||||
},
|
||||
} as Pref<string>,
|
||||
neverShowDonationInfo: {
|
||||
default: null as null | 'true',
|
||||
default: false,
|
||||
onSet: neverShowDonationInfo => {
|
||||
if (neverShowDonationInfo) {
|
||||
miLocalStorage.setItem('neverShowDonationInfo', 'true');
|
||||
} else {
|
||||
miLocalStorage.removeItem('neverShowDonationInfo');
|
||||
}
|
||||
},
|
||||
} as Pref<boolean>,
|
||||
neverShowLocalOnlyInfo: {
|
||||
default: null as null | 'true',
|
||||
default: false,
|
||||
onSet: neverShowLocalOnlyInfo => {
|
||||
if (neverShowLocalOnlyInfo) {
|
||||
miLocalStorage.setItem('neverShowLocalOnlyInfo', 'true');
|
||||
} else {
|
||||
miLocalStorage.removeItem('neverShowLocalOnlyInfo');
|
||||
}
|
||||
},
|
||||
} as Pref<boolean>,
|
||||
//#endregion
|
||||
} satisfies PreferencesDefinition;
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { computed, onUnmounted, ref, watch } from 'vue';
|
||||
import { computed, nextTick, onUnmounted, ref, watch } from 'vue';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { host, version } from '@@/js/config.js';
|
||||
import { PREF_DEF } from './def.js';
|
||||
|
@ -14,6 +14,7 @@ import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
|
|||
import { i18n } from '@/i18n.js';
|
||||
import * as os from '@/os.js';
|
||||
import { deepEqual } from '@/utility/deep-equal.js';
|
||||
import { reloadAsk } from '@/utility/reload-ask';
|
||||
|
||||
// NOTE: 明示的な設定値のひとつとして null もあり得るため、設定が存在しないかどうかを判定する目的で null で比較したり ?? を使ってはいけない
|
||||
|
||||
|
@ -84,16 +85,29 @@ export type StorageProvider = {
|
|||
cloudSet: <K extends keyof PREF>(ctx: { key: K; scope: Scope; value: ValueOf<K>; }) => Promise<void>;
|
||||
};
|
||||
|
||||
export type PreferencesDefinition = Record<string, {
|
||||
default: any;
|
||||
export type Pref<T> = {
|
||||
default: T;
|
||||
accountDependent?: boolean;
|
||||
serverDependent?: boolean;
|
||||
}>;
|
||||
needsReload?: boolean;
|
||||
onSet?: (value: T) => void;
|
||||
};
|
||||
|
||||
export type PreferencesDefinition = Record<string, Pref<any> | undefined>;
|
||||
|
||||
export class PreferencesManager {
|
||||
private storageProvider: StorageProvider;
|
||||
public profile: PreferencesProfile;
|
||||
public cloudReady: Promise<void>;
|
||||
private enableReload = true;
|
||||
|
||||
public suppressReload() {
|
||||
this.enableReload = false;
|
||||
}
|
||||
|
||||
public allowReload() {
|
||||
this.enableReload = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* static / state の略 (static が予約語のため)
|
||||
|
@ -126,11 +140,11 @@ export class PreferencesManager {
|
|||
}
|
||||
|
||||
private isAccountDependentKey<K extends keyof PREF>(key: K): boolean {
|
||||
return (PREF_DEF as PreferencesDefinition)[key].accountDependent === true;
|
||||
return (PREF_DEF as PreferencesDefinition)[key]?.accountDependent === true;
|
||||
}
|
||||
|
||||
private isServerDependentKey<K extends keyof PREF>(key: K): boolean {
|
||||
return (PREF_DEF as PreferencesDefinition)[key].serverDependent === true;
|
||||
return (PREF_DEF as PreferencesDefinition)[key]?.serverDependent === true;
|
||||
}
|
||||
|
||||
private rewriteRawState<K extends keyof PREF>(key: K, value: ValueOf<K>) {
|
||||
|
@ -150,6 +164,20 @@ export class PreferencesManager {
|
|||
|
||||
this.rewriteRawState(key, v);
|
||||
|
||||
const pref = (PREF_DEF as PreferencesDefinition)[key];
|
||||
if (pref) {
|
||||
// Call custom setter
|
||||
if (pref.onSet) {
|
||||
pref.onSet(v);
|
||||
}
|
||||
|
||||
// Prompt to reload the frontend
|
||||
if (pref.needsReload && this.enableReload) {
|
||||
// noinspection JSIgnoredPromiseFromCall
|
||||
nextTick(() => reloadAsk({ unison: true }));
|
||||
}
|
||||
}
|
||||
|
||||
const record = this.getMatchedRecordOf(key);
|
||||
|
||||
if (parseScope(record[0]).account == null && this.isAccountDependentKey(key)) {
|
||||
|
|
|
@ -12922,7 +12922,11 @@ export type operations = {
|
|||
requestBody: {
|
||||
content: {
|
||||
'application/json': {
|
||||
uri: string;
|
||||
uri?: string | null;
|
||||
/** Format: misskey:id */
|
||||
userId?: string | null;
|
||||
/** Format: misskey:id */
|
||||
noteId?: string | null;
|
||||
expandCollectionItems?: boolean;
|
||||
expandCollectionLimit?: number | null;
|
||||
allowAnonymous?: boolean;
|
||||
|
@ -24263,7 +24267,7 @@ export type operations = {
|
|||
/** @description OK (with results) */
|
||||
200: {
|
||||
content: {
|
||||
'application/json': Record<string, never>;
|
||||
'application/json': unknown;
|
||||
};
|
||||
};
|
||||
/** @description Client error */
|
||||
|
|
|
@ -624,3 +624,7 @@ keepCwEnabled: "Enabled (copy CWs verbatim)"
|
|||
keepCwPrependRe: "Enabled (copy CW and prepend \"RE:\")"
|
||||
|
||||
noteFooterLabel: "Note controls"
|
||||
|
||||
rawUserDescription: "Packed user data in its raw form. Most of these fields are public and visible to all users."
|
||||
rawInfoDescription: "Extended user data in its raw form. These fields are private and can only be accessed by moderators."
|
||||
rawApDescription: "ActivityPub user data in its raw form. These fields are public and accessible to other instances."
|
||||
|
|
Loading…
Add table
Reference in a new issue