Merge remote-tracking branch 'upstream/develop' into nodeinfostats
This commit is contained in:
commit
326bc0a24f
27 changed files with 179 additions and 148 deletions
|
|
@ -341,9 +341,9 @@ checkActivityPubGetSignature: false
|
|||
# Upload or download file size limits (bytes)
|
||||
#maxFileSize: 262144000
|
||||
|
||||
# timeout and maximum size for imports (e.g. note imports)
|
||||
# timeout (in milliseconds) and maximum size for imports (e.g. note imports)
|
||||
#import:
|
||||
# downloadTimeout: 30
|
||||
# downloadTimeout: 30000
|
||||
# maxFileSize: 262144000
|
||||
|
||||
# PID File of master process
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ stages:
|
|||
|
||||
testCommit:
|
||||
stage: test
|
||||
image: node:iron
|
||||
image: node:jod
|
||||
services:
|
||||
- postgres:15
|
||||
- redis
|
||||
|
|
|
|||
16
locales/index.d.ts
vendored
16
locales/index.d.ts
vendored
|
|
@ -9020,6 +9020,10 @@ export interface Locale extends ILocale {
|
|||
* Remove background
|
||||
*/
|
||||
"removeBackground": string;
|
||||
/**
|
||||
* ListenBrainz username
|
||||
*/
|
||||
"listenbrainz": string;
|
||||
};
|
||||
"_exportOrImport": {
|
||||
/**
|
||||
|
|
@ -10805,6 +10809,10 @@ export interface Locale extends ILocale {
|
|||
* Date
|
||||
*/
|
||||
"date": string;
|
||||
/**
|
||||
* Boost (hold Shift for visibility menu)
|
||||
*/
|
||||
"renoteShift": string;
|
||||
/**
|
||||
* Quoted.
|
||||
*/
|
||||
|
|
@ -11525,6 +11533,14 @@ export interface Locale extends ILocale {
|
|||
* Change the background color of text.
|
||||
*/
|
||||
"backgroundDescription": string;
|
||||
/**
|
||||
* Border
|
||||
*/
|
||||
"border": string;
|
||||
/**
|
||||
* Draw a border around the content.
|
||||
*/
|
||||
"borderDescription": string;
|
||||
/**
|
||||
* Plain
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
"private": true,
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": "^20.10.0 || ^22.0.0"
|
||||
"node": "^22.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "node ./built/boot/entry.js",
|
||||
|
|
|
|||
|
|
@ -528,44 +528,44 @@ export class NoteEntityService implements OnModuleInit {
|
|||
// パフォーマンスのためノートが作成されてから2秒以上経っていない場合はリアクションを取得しない
|
||||
const oldId = this.idService.gen(Date.now() - 2000);
|
||||
|
||||
const targetNotes: MiNote[] = [];
|
||||
for (const note of notes) {
|
||||
if (isPureRenote(note)) {
|
||||
const reactionsCount = Object.values(this.reactionsBufferingService.mergeReactions(note.renote.reactions, bufferedReactions?.get(note.renote.id)?.deltas ?? {})).reduce((a, b) => a + b, 0);
|
||||
if (reactionsCount === 0) {
|
||||
myReactionsMap.set(note.renote.id, null);
|
||||
} else if (reactionsCount <= note.renote.reactionAndUserPairCache.length + (bufferedReactions?.get(note.renote.id)?.pairs.length ?? 0)) {
|
||||
const pairInBuffer = bufferedReactions?.get(note.renote.id)?.pairs.find(p => p[0] === meId);
|
||||
if (pairInBuffer) {
|
||||
myReactionsMap.set(note.renote.id, pairInBuffer[1]);
|
||||
} else {
|
||||
const pair = note.renote.reactionAndUserPairCache.find(p => p.startsWith(meId));
|
||||
myReactionsMap.set(note.renote.id, pair ? pair.split('/')[1] : null);
|
||||
}
|
||||
} else {
|
||||
idsNeedFetchMyReaction.add(note.renote.id);
|
||||
// we may need to fetch 'my reaction' for renote target.
|
||||
targetNotes.push(note.renote);
|
||||
if (note.renote.reply) {
|
||||
// idem if the renote is also a reply.
|
||||
targetNotes.push(note.renote.reply);
|
||||
}
|
||||
} else if (note.reply) {
|
||||
// idem for OP of a regular reply.
|
||||
targetNotes.push(note.reply);
|
||||
} else {
|
||||
if (note.id < oldId) {
|
||||
const reactionsCount = Object.values(this.reactionsBufferingService.mergeReactions(note.reactions, bufferedReactions?.get(note.id)?.deltas ?? {})).reduce((a, b) => a + b, 0);
|
||||
if (reactionsCount === 0) {
|
||||
myReactionsMap.set(note.id, null);
|
||||
} else if (reactionsCount <= note.reactionAndUserPairCache.length + (bufferedReactions?.get(note.id)?.pairs.length ?? 0)) {
|
||||
const pairInBuffer = bufferedReactions?.get(note.id)?.pairs.find(p => p[0] === meId);
|
||||
if (pairInBuffer) {
|
||||
myReactionsMap.set(note.id, pairInBuffer[1]);
|
||||
} else {
|
||||
const pair = note.reactionAndUserPairCache.find(p => p.startsWith(meId));
|
||||
myReactionsMap.set(note.id, pair ? pair.split('/')[1] : null);
|
||||
}
|
||||
} else {
|
||||
idsNeedFetchMyReaction.add(note.id);
|
||||
}
|
||||
targetNotes.push(note);
|
||||
} else {
|
||||
myReactionsMap.set(note.id, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const note of targetNotes) {
|
||||
const reactionsCount = Object.values(this.reactionsBufferingService.mergeReactions(note.reactions, bufferedReactions?.get(note.id)?.deltas ?? {})).reduce((a, b) => a + b, 0);
|
||||
if (reactionsCount === 0) {
|
||||
myReactionsMap.set(note.id, null);
|
||||
} else if (reactionsCount <= note.reactionAndUserPairCache.length + (bufferedReactions?.get(note.id)?.pairs.length ?? 0)) {
|
||||
const pairInBuffer = bufferedReactions?.get(note.id)?.pairs.find(p => p[0] === meId);
|
||||
if (pairInBuffer) {
|
||||
myReactionsMap.set(note.id, pairInBuffer[1]);
|
||||
} else {
|
||||
const pair = note.reactionAndUserPairCache.find(p => p.startsWith(meId));
|
||||
myReactionsMap.set(note.id, pair ? pair.split('/')[1] : null);
|
||||
}
|
||||
} else {
|
||||
idsNeedFetchMyReaction.add(note.id);
|
||||
}
|
||||
}
|
||||
|
||||
const myReactions = idsNeedFetchMyReaction.size > 0 ? await this.noteReactionsRepository.findBy({
|
||||
userId: meId,
|
||||
noteId: In(Array.from(idsNeedFetchMyReaction)),
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import { bindThis } from '@/decorators.js';
|
|||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||
import type * as Bull from 'bullmq';
|
||||
import type { DbUserImportJobData } from '../types.js';
|
||||
import type { Config } from '@/config.js';
|
||||
|
||||
// TODO: 名前衝突時の動作を選べるようにする
|
||||
@Injectable()
|
||||
|
|
@ -24,6 +25,9 @@ export class ImportCustomEmojisProcessorService {
|
|||
private logger: Logger;
|
||||
|
||||
constructor(
|
||||
@Inject(DI.config)
|
||||
private config: Config,
|
||||
|
||||
@Inject(DI.driveFilesRepository)
|
||||
private driveFilesRepository: DriveFilesRepository,
|
||||
|
||||
|
|
@ -57,7 +61,7 @@ export class ImportCustomEmojisProcessorService {
|
|||
|
||||
try {
|
||||
fs.writeFileSync(destPath, '', 'binary');
|
||||
await this.downloadService.downloadUrl(file.url, destPath);
|
||||
await this.downloadService.downloadUrl(file.url, destPath, { operationTimeout: this.config.import?.downloadTimeout, maxSize: this.config.import?.maxFileSize });
|
||||
} catch (e) { // TODO: 何度か再試行
|
||||
if (e instanceof Error || typeof e === 'string') {
|
||||
this.logger.error(e);
|
||||
|
|
|
|||
|
|
@ -626,7 +626,7 @@ export class ImportNotesProcessorService {
|
|||
|
||||
if (!exists) {
|
||||
try {
|
||||
await this.downloadService.downloadUrl(videos[0].url, filePath);
|
||||
await this.downloadUrl(videos[0].url, filePath);
|
||||
} catch (e) { // TODO: 何度か再試行
|
||||
this.logger.error(e instanceof Error ? e : new Error(e as string));
|
||||
}
|
||||
|
|
@ -651,7 +651,7 @@ export class ImportNotesProcessorService {
|
|||
|
||||
if (!exists) {
|
||||
try {
|
||||
await this.downloadService.downloadUrl(file.media_url_https, filePath);
|
||||
await this.downloadUrl(file.media_url_https, filePath);
|
||||
} catch (e) { // TODO: 何度か再試行
|
||||
this.logger.error(e instanceof Error ? e : new Error(e as string));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -68,6 +68,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
for (let i = 0; i < ps.count; i++) {
|
||||
ticketsPromises.push(this.registrationTicketsRepository.insertOne({
|
||||
id: this.idService.gen(),
|
||||
createdBy: me,
|
||||
createdById: me.id,
|
||||
expiresAt: ps.expiresAt ? new Date(ps.expiresAt) : null,
|
||||
code: generateInviteCode(),
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { IsNull } from 'typeorm';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import type { EmojisRepository } from '@/models/_.js';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
|
|
@ -59,7 +58,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
const emojis = await this.emojisRepository.createQueryBuilder()
|
||||
.where('host IS NULL')
|
||||
.orderBy('LOWER(category)', 'ASC')
|
||||
.orderBy('LOWER(name)', 'ASC')
|
||||
.addOrderBy('LOWER(name)', 'ASC')
|
||||
.getMany();
|
||||
return {
|
||||
emojis: await this.emojiEntityService.packSimpleMany(emojis),
|
||||
|
|
|
|||
|
|
@ -103,11 +103,42 @@ export default abstract class Channel {
|
|||
|
||||
public onMessage?(type: string, body: JsonValue): void;
|
||||
|
||||
public async assignMyReaction(note: Packed<'Note'>, noteEntityService: NoteEntityService) {
|
||||
if (this.user && Object.keys(note.reactions).length > 0) {
|
||||
const myReaction = await noteEntityService.populateMyReaction(note, this.user.id);
|
||||
note.myReaction = myReaction;
|
||||
public async assignMyReaction(note: Packed<'Note'>, noteEntityService: NoteEntityService): Promise<Packed<'Note'>> {
|
||||
let changed = false;
|
||||
// StreamingApiServerService creates a single EventEmitter per server process,
|
||||
// so a new note arriving from redis gets de-serialised once per server process,
|
||||
// and then that single object is passed to all active channels on each connection.
|
||||
// If we didn't clone the notes here, different connections would asynchronously write
|
||||
// different values to the same object, resulting in a random value being sent to each frontend. -- Dakkar
|
||||
const clonedNote = { ...note };
|
||||
if (this.user && isRenotePacked(note) && !isQuotePacked(note)) {
|
||||
if (note.renote && Object.keys(note.renote.reactions).length > 0) {
|
||||
const myReaction = await noteEntityService.populateMyReaction(note.renote, this.user.id);
|
||||
if (myReaction) {
|
||||
changed = true;
|
||||
clonedNote.renote = { ...note.renote };
|
||||
clonedNote.renote.myReaction = myReaction;
|
||||
}
|
||||
}
|
||||
if (note.renote?.reply && Object.keys(note.renote.reply.reactions).length > 0) {
|
||||
const myReaction = await noteEntityService.populateMyReaction(note.renote.reply, this.user.id);
|
||||
if (myReaction) {
|
||||
changed = true;
|
||||
clonedNote.renote = { ...note.renote };
|
||||
clonedNote.renote.reply = { ...note.renote.reply };
|
||||
clonedNote.renote.reply.myReaction = myReaction;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this.user && note.reply && Object.keys(note.reply.reactions).length > 0) {
|
||||
const myReaction = await noteEntityService.populateMyReaction(note.reply, this.user.id);
|
||||
if (myReaction) {
|
||||
changed = true;
|
||||
clonedNote.reply = { ...note.reply };
|
||||
clonedNote.reply.myReaction = myReaction;
|
||||
}
|
||||
}
|
||||
return changed ? clonedNote : note;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -65,24 +65,11 @@ class BubbleTimelineChannel extends Channel {
|
|||
|
||||
if (this.isNoteMutedOrBlocked(note)) return;
|
||||
|
||||
const reactionsToFetch = [];
|
||||
if (this.user && isRenotePacked(note) && !isQuotePacked(note)) {
|
||||
if (note.renote) {
|
||||
reactionsToFetch.push(this.assignMyReaction(note.renote, this.noteEntityService));
|
||||
if (note.renote.reply) {
|
||||
reactionsToFetch.push(this.assignMyReaction(note.renote.reply, this.noteEntityService));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this.user && note.reply) {
|
||||
reactionsToFetch.push(this.assignMyReaction(note.reply, this.noteEntityService));
|
||||
}
|
||||
const clonedNote = await this.assignMyReaction(note, this.noteEntityService);
|
||||
|
||||
await Promise.all(reactionsToFetch);
|
||||
this.connection.cacheNote(clonedNote);
|
||||
|
||||
this.connection.cacheNote(note);
|
||||
|
||||
this.send('note', note);
|
||||
this.send('note', clonedNote);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
|
|
|||
|
|
@ -60,24 +60,11 @@ class GlobalTimelineChannel extends Channel {
|
|||
|
||||
if (this.isNoteMutedOrBlocked(note)) return;
|
||||
|
||||
const reactionsToFetch = [];
|
||||
if (this.user && isRenotePacked(note) && !isQuotePacked(note)) {
|
||||
if (note.renote) {
|
||||
reactionsToFetch.push(this.assignMyReaction(note.renote, this.noteEntityService));
|
||||
if (note.renote.reply) {
|
||||
reactionsToFetch.push(this.assignMyReaction(note.renote.reply, this.noteEntityService));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this.user && note.reply) {
|
||||
reactionsToFetch.push(this.assignMyReaction(note.reply, this.noteEntityService));
|
||||
}
|
||||
const clonedNote = await this.assignMyReaction(note, this.noteEntityService);
|
||||
|
||||
await Promise.all(reactionsToFetch);
|
||||
this.connection.cacheNote(clonedNote);
|
||||
|
||||
this.connection.cacheNote(note);
|
||||
|
||||
this.send('note', note);
|
||||
this.send('note', clonedNote);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
|
|
|||
|
|
@ -81,24 +81,11 @@ class HomeTimelineChannel extends Channel {
|
|||
|
||||
if (this.isNoteMutedOrBlocked(note)) return;
|
||||
|
||||
const reactionsToFetch = [];
|
||||
if (this.user && isRenotePacked(note) && !isQuotePacked(note)) {
|
||||
if (note.renote) {
|
||||
reactionsToFetch.push(this.assignMyReaction(note.renote, this.noteEntityService));
|
||||
if (note.renote.reply) {
|
||||
reactionsToFetch.push(this.assignMyReaction(note.renote.reply, this.noteEntityService));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this.user && note.reply) {
|
||||
reactionsToFetch.push(this.assignMyReaction(note.reply, this.noteEntityService));
|
||||
}
|
||||
const clonedNote = await this.assignMyReaction(note, this.noteEntityService);
|
||||
|
||||
await Promise.all(reactionsToFetch);
|
||||
this.connection.cacheNote(clonedNote);
|
||||
|
||||
this.connection.cacheNote(note);
|
||||
|
||||
this.send('note', note);
|
||||
this.send('note', clonedNote);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
|
|
|||
|
|
@ -98,24 +98,11 @@ class HybridTimelineChannel extends Channel {
|
|||
}
|
||||
}
|
||||
|
||||
const reactionsToFetch = [];
|
||||
if (this.user && isRenotePacked(note) && !isQuotePacked(note)) {
|
||||
if (note.renote) {
|
||||
reactionsToFetch.push(this.assignMyReaction(note.renote, this.noteEntityService));
|
||||
if (note.renote.reply) {
|
||||
reactionsToFetch.push(this.assignMyReaction(note.renote.reply, this.noteEntityService));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this.user && note.reply) {
|
||||
reactionsToFetch.push(this.assignMyReaction(note.reply, this.noteEntityService));
|
||||
}
|
||||
const clonedNote = await this.assignMyReaction(note, this.noteEntityService);
|
||||
|
||||
await Promise.all(reactionsToFetch);
|
||||
this.connection.cacheNote(clonedNote);
|
||||
|
||||
this.connection.cacheNote(note);
|
||||
|
||||
this.send('note', note);
|
||||
this.send('note', clonedNote);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
|
|
|||
|
|
@ -70,24 +70,11 @@ class LocalTimelineChannel extends Channel {
|
|||
|
||||
if (this.isNoteMutedOrBlocked(note)) return;
|
||||
|
||||
const reactionsToFetch = [];
|
||||
if (this.user && isRenotePacked(note) && !isQuotePacked(note)) {
|
||||
if (note.renote) {
|
||||
reactionsToFetch.push(this.assignMyReaction(note.renote, this.noteEntityService));
|
||||
if (note.renote.reply) {
|
||||
reactionsToFetch.push(this.assignMyReaction(note.renote.reply, this.noteEntityService));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this.user && note.reply) {
|
||||
reactionsToFetch.push(this.assignMyReaction(note.reply, this.noteEntityService));
|
||||
}
|
||||
const clonedNote = await this.assignMyReaction(note, this.noteEntityService);
|
||||
|
||||
await Promise.all(reactionsToFetch);
|
||||
this.connection.cacheNote(clonedNote);
|
||||
|
||||
this.connection.cacheNote(note);
|
||||
|
||||
this.send('note', note);
|
||||
this.send('note', clonedNote);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
|
|
|||
|
|
@ -186,11 +186,13 @@ function parseAndMergeCategories(input: string, root: CustomEmojiFolderTree): Cu
|
|||
const parts = input.split('/').map(p => p.trim());
|
||||
let currentNode: CustomEmojiFolderTree = root;
|
||||
|
||||
const currentPath = [];
|
||||
for (const part of parts) {
|
||||
currentPath.push(part);
|
||||
let existingNode = currentNode.children.find((node) => node.value === part);
|
||||
|
||||
if (!existingNode) {
|
||||
const newNode: CustomEmojiFolderTree = { value: part, category: input, children: [] };
|
||||
const newNode: CustomEmojiFolderTree = { value: part, category: currentPath.join("/"), children: [] };
|
||||
currentNode.children.push(newNode);
|
||||
existingNode = newNode;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -128,11 +128,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<button
|
||||
v-if="canRenote"
|
||||
ref="renoteButton"
|
||||
v-tooltip="renoteTooltip"
|
||||
:class="$style.footerButton"
|
||||
class="_button"
|
||||
:style="renoted ? 'color: var(--MI_THEME-accent) !important;' : ''"
|
||||
@click.stop
|
||||
@mousedown.prevent="renoted ? undoRenote(appearNote) : boostVisibility()"
|
||||
@mousedown.prevent="renoted ? undoRenote(appearNote) : boostVisibility($event.shiftKey)"
|
||||
>
|
||||
<i class="ti ti-repeat"></i>
|
||||
<p v-if="appearNote.renoteCount > 0" :class="$style.footerButtonCount">{{ number(appearNote.renoteCount) }}</p>
|
||||
|
|
@ -238,7 +239,7 @@ import { getNoteSummary } from '@/scripts/get-note-summary.js';
|
|||
import MkRippleEffect from '@/components/MkRippleEffect.vue';
|
||||
import { showMovedDialog } from '@/scripts/show-moved-dialog.js';
|
||||
import { useRouter } from '@/router/supplier.js';
|
||||
import { boostMenuItems, type Visibility } from '@/scripts/boost-quote.js';
|
||||
import { boostMenuItems, type Visibility, computeRenoteTooltip } from '@/scripts/boost-quote.js';
|
||||
import { isEnabledUrlPreview } from '@/instance.js';
|
||||
import { type Keymap } from '@/scripts/hotkey.js';
|
||||
import { focusPrev, focusNext } from '@/scripts/focus.js';
|
||||
|
|
@ -338,6 +339,8 @@ const pleaseLoginContext = computed<OpenOnRemoteOptions>(() => ({
|
|||
url: `https://${host}/notes/${appearNote.value.id}`,
|
||||
}));
|
||||
|
||||
const renoteTooltip = computeRenoteTooltip(renoted);
|
||||
|
||||
/* Overload FunctionにLintが対応していないのでコメントアウト
|
||||
function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly: true): boolean;
|
||||
function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly: false): boolean | 'sensitiveMute';
|
||||
|
|
@ -506,10 +509,10 @@ if (!props.mock) {
|
|||
}
|
||||
}
|
||||
|
||||
function boostVisibility() {
|
||||
function boostVisibility(forceMenu: boolean = false) {
|
||||
if (renoting) return;
|
||||
|
||||
if (!defaultStore.state.showVisibilitySelectorOnBoost) {
|
||||
if (!defaultStore.state.showVisibilitySelectorOnBoost && !forceMenu) {
|
||||
renote(defaultStore.state.visibilityOnBoost);
|
||||
} else {
|
||||
os.popupMenu(boostMenuItems(appearNote, renote), renoteButton.value);
|
||||
|
|
|
|||
|
|
@ -140,10 +140,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<button
|
||||
v-if="canRenote"
|
||||
ref="renoteButton"
|
||||
v-tooltip="renoteTooltip"
|
||||
class="_button"
|
||||
:class="$style.noteFooterButton"
|
||||
:style="renoted ? 'color: var(--MI_THEME-accent) !important;' : ''"
|
||||
@mousedown.prevent="renoted ? undoRenote() : boostVisibility()"
|
||||
@mousedown.prevent="renoted ? undoRenote() : boostVisibility($event.shiftKey)"
|
||||
>
|
||||
<i class="ti ti-repeat"></i>
|
||||
<p v-if="appearNote.renoteCount > 0" :class="$style.noteFooterButtonCount">{{ number(appearNote.renoteCount) }}</p>
|
||||
|
|
@ -280,7 +281,7 @@ import MkUserCardMini from '@/components/MkUserCardMini.vue';
|
|||
import MkPagination, { type Paging } from '@/components/MkPagination.vue';
|
||||
import MkReactionIcon from '@/components/MkReactionIcon.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { boostMenuItems, type Visibility } from '@/scripts/boost-quote.js';
|
||||
import { boostMenuItems, type Visibility, computeRenoteTooltip } from '@/scripts/boost-quote.js';
|
||||
import { isEnabledUrlPreview } from '@/instance.js';
|
||||
import { getAppearNote } from '@/scripts/get-appear-note.js';
|
||||
import { type Keymap } from '@/scripts/hotkey.js';
|
||||
|
|
@ -347,6 +348,8 @@ const quotes = ref<Misskey.entities.Note[]>([]);
|
|||
const canRenote = computed(() => ['public', 'home'].includes(appearNote.value.visibility) || (appearNote.value.visibility === 'followers' && appearNote.value.userId === $i?.id));
|
||||
const defaultLike = computed(() => defaultStore.state.like ? defaultStore.state.like : null);
|
||||
|
||||
const renoteTooltip = computeRenoteTooltip(renoted);
|
||||
|
||||
watch(() => props.expandAllCws, (expandAllCws) => {
|
||||
if (expandAllCws !== showContent.value) showContent.value = expandAllCws;
|
||||
});
|
||||
|
|
@ -478,10 +481,10 @@ useTooltip(quoteButton, async (showing) => {
|
|||
});
|
||||
});
|
||||
|
||||
function boostVisibility() {
|
||||
function boostVisibility(forceMenu: boolean = false) {
|
||||
if (renoting) return;
|
||||
|
||||
if (!defaultStore.state.showVisibilitySelectorOnBoost) {
|
||||
if (!defaultStore.state.showVisibilitySelectorOnBoost && !forceMenu) {
|
||||
renote(defaultStore.state.visibilityOnBoost);
|
||||
} else {
|
||||
os.popupMenu(boostMenuItems(appearNote, renote), renoteButton.value);
|
||||
|
|
|
|||
|
|
@ -28,10 +28,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<button
|
||||
v-if="canRenote"
|
||||
ref="renoteButton"
|
||||
v-tooltip="renoteTooltip"
|
||||
class="_button"
|
||||
:class="$style.noteFooterButton"
|
||||
:style="renoted ? 'color: var(--MI_THEME-accent) !important;' : ''"
|
||||
@mousedown="renoted ? undoRenote() : boostVisibility()"
|
||||
@mousedown="renoted ? undoRenote() : boostVisibility($event.shiftKey)"
|
||||
>
|
||||
<i class="ph-rocket-launch ph-bold ph-lg"></i>
|
||||
<p v-if="note.renoteCount > 0" :class="$style.noteFooterButtonCount">{{ note.renoteCount }}</p>
|
||||
|
|
@ -106,7 +107,7 @@ import { reactionPicker } from '@/scripts/reaction-picker.js';
|
|||
import { claimAchievement } from '@/scripts/achievements.js';
|
||||
import { getNoteMenu } from '@/scripts/get-note-menu.js';
|
||||
import { useNoteCapture } from '@/scripts/use-note-capture.js';
|
||||
import { boostMenuItems, type Visibility } from '@/scripts/boost-quote.js';
|
||||
import { boostMenuItems, type Visibility, computeRenoteTooltip } from '@/scripts/boost-quote.js';
|
||||
|
||||
const canRenote = computed(() => ['public', 'home'].includes(props.note.visibility) || props.note.userId === $i.id);
|
||||
|
||||
|
|
@ -135,6 +136,8 @@ const quoteButton = shallowRef<HTMLElement>();
|
|||
const menuButton = shallowRef<HTMLElement>();
|
||||
const likeButton = shallowRef<HTMLElement>();
|
||||
|
||||
const renoteTooltip = computeRenoteTooltip(computed);
|
||||
|
||||
let appearNote = computed(() => isRenote ? props.note.renote as Misskey.entities.Note : props.note);
|
||||
const defaultLike = computed(() => defaultStore.state.like ? defaultStore.state.like : null);
|
||||
const replies = ref<Misskey.entities.Note[]>([]);
|
||||
|
|
@ -285,8 +288,8 @@ watch(() => props.expandAllCws, (expandAllCws) => {
|
|||
if (expandAllCws !== showContent.value) showContent.value = expandAllCws;
|
||||
});
|
||||
|
||||
function boostVisibility() {
|
||||
if (!defaultStore.state.showVisibilitySelectorOnBoost) {
|
||||
function boostVisibility(forceMenu: boolean = false) {
|
||||
if (!defaultStore.state.showVisibilitySelectorOnBoost && !forceMenu) {
|
||||
renote(defaultStore.state.visibilityOnBoost);
|
||||
} else {
|
||||
os.popupMenu(boostMenuItems(appearNote, renote), renoteButton.value);
|
||||
|
|
|
|||
|
|
@ -381,6 +381,16 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ i18n.ts._mfm.border }}</div>
|
||||
<div class="content">
|
||||
<p>{{ i18n.ts._mfm.borderDescription }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_border"/>
|
||||
<MkTextarea v-model="preview_border"><span>MFM</span></MkTextarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ i18n.ts._mfm.plain }}</div>
|
||||
<div class="content">
|
||||
|
|
@ -421,7 +431,7 @@ const preview_center = ref(
|
|||
);
|
||||
const preview_inlineCode = ref('`<: "Hello, world!"`');
|
||||
const preview_blockCode = ref(
|
||||
'```\n~ (#i, 100) {\n\t<: ? ((i % 15) = 0) "FizzBuzz"\n\t\t.? ((i % 3) = 0) "Fizz"\n\t\t.? ((i % 5) = 0) "Buzz"\n\t\t. i\n}\n```',
|
||||
'```ai\n~ (#i, 100) {\n\t<: ? ((i % 15) = 0) "FizzBuzz"\n\t\t.? ((i % 3) = 0) "Fizz"\n\t\t.? ((i % 5) = 0) "Buzz"\n\t\t. i\n}\n```',
|
||||
);
|
||||
const preview_inlineMath = ref(
|
||||
'\\(x= \\frac{-b\' \\pm \\sqrt{(b\')^2-ac}}{a}\\)',
|
||||
|
|
@ -479,6 +489,7 @@ const preview_scale = ref(
|
|||
);
|
||||
const preview_fg = ref('$[fg.color=eb6f92 Text color]');
|
||||
const preview_bg = ref('$[bg.color=31748f Background color]');
|
||||
const preview_border = ref('$[border.color=eb6f92,style=outset,width=10,radius=10 Border]');
|
||||
const preview_plain = ref(
|
||||
'<plain>**bold** @mention #hashtag `code` $[x2 🍮]</plain>',
|
||||
);
|
||||
|
|
|
|||
|
|
@ -129,11 +129,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<button
|
||||
v-if="canRenote"
|
||||
ref="renoteButton"
|
||||
v-tooltip="renoteTooltip"
|
||||
:class="$style.footerButton"
|
||||
class="_button"
|
||||
:style="renoted ? 'color: var(--MI_THEME-accent) !important;' : ''"
|
||||
@click.stop
|
||||
@mousedown.prevent="renoted ? undoRenote(appearNote) : boostVisibility()"
|
||||
@mousedown.prevent="renoted ? undoRenote(appearNote) : boostVisibility($event.shiftKey)"
|
||||
>
|
||||
<i class="ti ti-repeat"></i>
|
||||
<p v-if="appearNote.renoteCount > 0" :class="$style.footerButtonCount">{{ number(appearNote.renoteCount) }}</p>
|
||||
|
|
@ -238,7 +239,7 @@ import { getNoteSummary } from '@/scripts/get-note-summary.js';
|
|||
import MkRippleEffect from '@/components/MkRippleEffect.vue';
|
||||
import { showMovedDialog } from '@/scripts/show-moved-dialog.js';
|
||||
import { useRouter } from '@/router/supplier.js';
|
||||
import { boostMenuItems, type Visibility } from '@/scripts/boost-quote.js';
|
||||
import { boostMenuItems, type Visibility, computeRenoteTooltip } from '@/scripts/boost-quote.js';
|
||||
import { isEnabledUrlPreview } from '@/instance.js';
|
||||
import { type Keymap } from '@/scripts/hotkey.js';
|
||||
import { focusPrev, focusNext } from '@/scripts/focus.js';
|
||||
|
|
@ -333,6 +334,8 @@ const defaultLike = computed(() => defaultStore.state.like ? defaultStore.state.
|
|||
const animated = computed(() => parsed.value ? checkAnimationFromMfm(parsed.value) : null);
|
||||
const allowAnim = ref(defaultStore.state.advancedMfm && defaultStore.state.animatedMfm ? true : false);
|
||||
|
||||
const renoteTooltip = computeRenoteTooltip(renoted);
|
||||
|
||||
const pleaseLoginContext = computed<OpenOnRemoteOptions>(() => ({
|
||||
type: 'lookup',
|
||||
url: `https://${host}/notes/${appearNote.value.id}`,
|
||||
|
|
@ -506,10 +509,10 @@ if (!props.mock) {
|
|||
}
|
||||
}
|
||||
|
||||
function boostVisibility() {
|
||||
function boostVisibility(forceMenu: boolean = false) {
|
||||
if (renoting) return;
|
||||
|
||||
if (!defaultStore.state.showVisibilitySelectorOnBoost) {
|
||||
if (!defaultStore.state.showVisibilitySelectorOnBoost && !forceMenu) {
|
||||
renote(defaultStore.state.visibilityOnBoost);
|
||||
} else {
|
||||
os.popupMenu(boostMenuItems(appearNote, renote), renoteButton.value);
|
||||
|
|
|
|||
|
|
@ -145,10 +145,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<button
|
||||
v-if="canRenote"
|
||||
ref="renoteButton"
|
||||
v-tooltip="renoteTooltip"
|
||||
class="_button"
|
||||
:class="$style.noteFooterButton"
|
||||
:style="renoted ? 'color: var(--MI_THEME-accent) !important;' : ''"
|
||||
@mousedown.prevent="renoted ? undoRenote() : boostVisibility()"
|
||||
@mousedown.prevent="renoted ? undoRenote() : boostVisibility($event.shiftKey)"
|
||||
>
|
||||
<i class="ti ti-repeat"></i>
|
||||
<p v-if="appearNote.renoteCount > 0" :class="$style.noteFooterButtonCount">{{ number(appearNote.renoteCount) }}</p>
|
||||
|
|
@ -285,7 +286,7 @@ import MkUserCardMini from '@/components/MkUserCardMini.vue';
|
|||
import MkPagination, { type Paging } from '@/components/MkPagination.vue';
|
||||
import MkReactionIcon from '@/components/MkReactionIcon.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { boostMenuItems, type Visibility } from '@/scripts/boost-quote.js';
|
||||
import { boostMenuItems, type Visibility, computeRenoteTooltip } from '@/scripts/boost-quote.js';
|
||||
import { isEnabledUrlPreview } from '@/instance.js';
|
||||
import { getAppearNote } from '@/scripts/get-appear-note.js';
|
||||
import { type Keymap } from '@/scripts/hotkey.js';
|
||||
|
|
@ -353,6 +354,8 @@ const quotes = ref<Misskey.entities.Note[]>([]);
|
|||
const canRenote = computed(() => ['public', 'home'].includes(appearNote.value.visibility) || (appearNote.value.visibility === 'followers' && appearNote.value.userId === $i?.id));
|
||||
const defaultLike = computed(() => defaultStore.state.like ? defaultStore.state.like : null);
|
||||
|
||||
const renoteTooltip = computeRenoteTooltip(renoted);
|
||||
|
||||
watch(() => props.expandAllCws, (expandAllCws) => {
|
||||
if (expandAllCws !== showContent.value) showContent.value = expandAllCws;
|
||||
});
|
||||
|
|
@ -484,10 +487,10 @@ useTooltip(quoteButton, async (showing) => {
|
|||
});
|
||||
});
|
||||
|
||||
function boostVisibility() {
|
||||
function boostVisibility(forceMenu: boolean = false) {
|
||||
if (renoting) return;
|
||||
|
||||
if (!defaultStore.state.showVisibilitySelectorOnBoost) {
|
||||
if (!defaultStore.state.showVisibilitySelectorOnBoost && !forceMenu) {
|
||||
renote(defaultStore.state.visibilityOnBoost);
|
||||
} else {
|
||||
os.popupMenu(boostMenuItems(appearNote, renote), renoteButton.value);
|
||||
|
|
|
|||
|
|
@ -36,10 +36,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<button
|
||||
v-if="canRenote"
|
||||
ref="renoteButton"
|
||||
v-tooltip="renoteTooltip"
|
||||
class="_button"
|
||||
:class="$style.noteFooterButton"
|
||||
:style="renoted ? 'color: var(--MI_THEME-accent) !important;' : ''"
|
||||
@mousedown="renoted ? undoRenote() : boostVisibility()"
|
||||
@mousedown="renoted ? undoRenote() : boostVisibility($event.shiftKey)"
|
||||
>
|
||||
<i class="ph-rocket-launch ph-bold ph-lg"></i>
|
||||
<p v-if="note.renoteCount > 0" :class="$style.noteFooterButtonCount">{{ note.renoteCount }}</p>
|
||||
|
|
@ -114,7 +115,7 @@ import { reactionPicker } from '@/scripts/reaction-picker.js';
|
|||
import { claimAchievement } from '@/scripts/achievements.js';
|
||||
import { getNoteMenu } from '@/scripts/get-note-menu.js';
|
||||
import { useNoteCapture } from '@/scripts/use-note-capture.js';
|
||||
import { boostMenuItems, type Visibility } from '@/scripts/boost-quote.js';
|
||||
import { boostMenuItems, type Visibility, computeRenoteTooltip } from '@/scripts/boost-quote.js';
|
||||
|
||||
const canRenote = computed(() => ['public', 'home'].includes(props.note.visibility) || props.note.userId === $i.id);
|
||||
const hideLine = computed(() => { return props.detail ? true : false; });
|
||||
|
|
@ -149,6 +150,8 @@ const quoteButton = shallowRef<HTMLElement>();
|
|||
const menuButton = shallowRef<HTMLElement>();
|
||||
const likeButton = shallowRef<HTMLElement>();
|
||||
|
||||
const renoteTooltip = computeRenoteTooltip(renoted);
|
||||
|
||||
let appearNote = computed(() => isRenote ? props.note.renote as Misskey.entities.Note : props.note);
|
||||
const defaultLike = computed(() => defaultStore.state.like ? defaultStore.state.like : null);
|
||||
const replies = ref<Misskey.entities.Note[]>([]);
|
||||
|
|
@ -299,8 +302,8 @@ watch(() => props.expandAllCws, (expandAllCws) => {
|
|||
if (expandAllCws !== showContent.value) showContent.value = expandAllCws;
|
||||
});
|
||||
|
||||
function boostVisibility() {
|
||||
if (!defaultStore.state.showVisibilitySelectorOnBoost) {
|
||||
function boostVisibility(forceMenu: boolean = false) {
|
||||
if (!defaultStore.state.showVisibilitySelectorOnBoost && !forceMenu) {
|
||||
renote(defaultStore.state.visibilityOnBoost);
|
||||
} else {
|
||||
os.popupMenu(boostMenuItems(appearNote, renote), renoteButton.value);
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</MkInput>
|
||||
|
||||
<MkInput v-model="profile.listenbrainz" manualSave>
|
||||
<template #label>ListenBrainz</template>
|
||||
<template #label>{{ i18n.ts._profile.listenbrainz }}</template>
|
||||
<template #prefix><i class="ph-headphones ph-bold ph-lg"></i></template>
|
||||
</MkInput>
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { ref, Ref } from 'vue';
|
||||
import { ref, Ref, computed, ComputedRef } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
|
|
@ -79,3 +79,11 @@ export function boostMenuItems(appearNote: Ref<Misskey.entities.Note>, renote: (
|
|||
} as MenuItem,
|
||||
];
|
||||
}
|
||||
|
||||
export function computeRenoteTooltip(renoted: Ref<boolean>): ComputedRef<string> {
|
||||
return computed(() => {
|
||||
if (renoted.value) return i18n.ts.unrenote;
|
||||
if (defaultStore.state.showVisibilitySelectorOnBoost) return i18n.ts.renote;
|
||||
return i18n.ts.renoteShift;
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ export default function sanitizeHtml(str: string | null): string | null {
|
|||
...original.defaults.allowedAttributes,
|
||||
a: original.defaults.allowedAttributes.a.concat(['style']),
|
||||
img: original.defaults.allowedAttributes.img.concat(['style']),
|
||||
'*': (original.defaults.allowedAttributes['*'] || []).concat(['style']),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ emailDestination: "Destination address"
|
|||
date: "Date"
|
||||
renote: "Boost"
|
||||
unrenote: "Remove boost"
|
||||
renoteShift: "Boost (hold Shift for visibility menu)"
|
||||
renoted: "Boosted."
|
||||
quoted: "Quoted."
|
||||
rmboost: "Unboosted."
|
||||
|
|
@ -270,6 +271,7 @@ _profile:
|
|||
changeBackground: "Change background"
|
||||
updateBackground: "Update background"
|
||||
removeBackground: "Remove background"
|
||||
listenbrainz: "ListenBrainz username"
|
||||
_timelines:
|
||||
bubble: "Bubble"
|
||||
_pages:
|
||||
|
|
@ -383,6 +385,8 @@ _mfm:
|
|||
fadeDescription: 'Fade text in and out.'
|
||||
background: "Background color"
|
||||
backgroundDescription: "Change the background color of text."
|
||||
border: "Border"
|
||||
borderDescription: "Draw a border around the content."
|
||||
plain: "Plain"
|
||||
plainDescription: "Deactivates the effects of all MFM contained within this MFM effect."
|
||||
_animatedMFM:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue