release: 2023.11.2
This commit is contained in:
commit
1022280465
@ -112,6 +112,7 @@ redis:
|
||||
# apiKey: ''
|
||||
# ssl: true
|
||||
# index: ''
|
||||
# scope: global
|
||||
|
||||
# ┌───────────────┐
|
||||
#───┘ ID generation └───────────────────────────────────────────
|
||||
@ -144,15 +145,22 @@ id: 'aidx'
|
||||
# Job concurrency per worker
|
||||
# deliverJobConcurrency: 128
|
||||
# inboxJobConcurrency: 16
|
||||
# relashionshipJobConcurrency: 16
|
||||
# What's relashionshipJob?:
|
||||
# Follow, unfollow, block and unblock(ings) while following-imports, etc. or account migrations.
|
||||
|
||||
# Job rate limiter
|
||||
# deliverJobPerSec: 128
|
||||
# inboxJobPerSec: 16
|
||||
# relashionshipJobPerSec: 64
|
||||
|
||||
# Job attempts
|
||||
# deliverJobMaxAttempts: 12
|
||||
# inboxJobMaxAttempts: 8
|
||||
|
||||
# Local address used for outgoing requests
|
||||
#outgoingAddress: 127.0.0.1
|
||||
|
||||
# IP address family used for outgoing request (ipv4, ipv6 or dual)
|
||||
#outgoingAddressFamily: ipv4
|
||||
|
||||
@ -175,8 +183,15 @@ proxyBypassHosts:
|
||||
#mediaProxy: https://example.com/proxy
|
||||
|
||||
# Proxy remote files (default: true)
|
||||
# Proxy remote files by this instance or mediaProxy to prevent remote files from running in remote domains.
|
||||
proxyRemoteFiles: true
|
||||
|
||||
# Movie Thumbnail Generation URL
|
||||
# There is no reference implementation.
|
||||
# For example, Misskey will point to the following URL:
|
||||
# https://example.com/thumbnail.webp?thumbnail=1&url=https%3A%2F%2Fstorage.example.com%2Fpath%2Fto%2Fvideo.mp4
|
||||
#videoThumbnailGenerator: https://example.com
|
||||
|
||||
# Sign to ActivityPub GET request (default: true)
|
||||
signToActivityPubGet: true
|
||||
|
||||
|
2
.github/workflows/welcome.yml
vendored
2
.github/workflows/welcome.yml
vendored
@ -8,7 +8,7 @@ jobs:
|
||||
run:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/first-interaction@v1.2.0
|
||||
- uses: actions/first-interaction@v1.3.0
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
issue-message: |
|
||||
|
@ -55,6 +55,7 @@ addToAntenna: "Add to antenna"
|
||||
sendMessage: "Send a message"
|
||||
copyRSS: "Copy RSS"
|
||||
copyUsername: "Copy username"
|
||||
openRemoteProfile: "Open remote profile"
|
||||
copyUserId: "Copy user ID"
|
||||
copyNoteId: "Copy note ID"
|
||||
copyFileId: "Copy file ID"
|
||||
@ -110,7 +111,6 @@ renote: "Boost"
|
||||
unrenote: "Remove boost"
|
||||
renoted: "Boosted."
|
||||
quoted: "Quoted."
|
||||
rmquote: "Removed quote."
|
||||
rmboost: "Unboosted."
|
||||
cantRenote: "This post can't be boosted."
|
||||
cantReRenote: "A boost can't be boosted."
|
||||
@ -891,6 +891,7 @@ continueThread: "View thread continuation"
|
||||
deleteAccountConfirm: "This will irreversibly delete your account. Proceed?"
|
||||
incorrectPassword: "Incorrect password."
|
||||
voteConfirm: "Confirm your vote for \"{choice}\"?"
|
||||
voteConfirmMulti: "Confirm your vote for \"{choice}\"?\n You can choose more options after confirmation."
|
||||
hide: "Hide"
|
||||
useDrawerReactionPickerForMobile: "Display reaction picker as drawer on mobile"
|
||||
welcomeBackWithName: "Welcome back, {name}"
|
||||
@ -987,6 +988,7 @@ cannotLoad: "Unable to load"
|
||||
numberOfProfileView: "Profile views"
|
||||
like: "Like"
|
||||
unlike: "Unlike"
|
||||
defaultLike: "Default like emoji"
|
||||
numberOfLikes: "Likes"
|
||||
show: "Show"
|
||||
neverShow: "Don't show again"
|
||||
@ -1855,6 +1857,14 @@ _ago:
|
||||
monthsAgo: "{n}mo ago"
|
||||
yearsAgo: "{n}y ago"
|
||||
invalid: "None"
|
||||
_timeIn:
|
||||
seconds: "in {n} seconds"
|
||||
minutes: "in {n} minutes"
|
||||
hours: "in {n} hours"
|
||||
days: "in {n} days"
|
||||
weeks: "in {n} weeks"
|
||||
months: "in {n} months"
|
||||
years: "in {n} years"
|
||||
_time:
|
||||
second: "Second(s)"
|
||||
minute: "Minute(s)"
|
||||
@ -1980,6 +1990,7 @@ _widgets:
|
||||
_userList:
|
||||
chooseList: "Select a list"
|
||||
clicker: "Clicker"
|
||||
search: "Search"
|
||||
_cw:
|
||||
hide: "Hide"
|
||||
show: "Show content"
|
||||
@ -2007,6 +2018,7 @@ _poll:
|
||||
remainingHours: "{h} hour(s) {m} minute(s) remaining"
|
||||
remainingMinutes: "{m} minute(s) {s} second(s) remaining"
|
||||
remainingSeconds: "{s} second(s) remaining"
|
||||
multiple: "Multiple choices"
|
||||
_visibility:
|
||||
public: "Public"
|
||||
publicDescription: "Your note will be visible for all users"
|
||||
|
6
locales/index.d.ts
vendored
6
locales/index.d.ts
vendored
@ -58,6 +58,7 @@ export interface Locale {
|
||||
"sendMessage": string;
|
||||
"copyRSS": string;
|
||||
"copyUsername": string;
|
||||
"openRemoteProfile": string;
|
||||
"copyUserId": string;
|
||||
"copyNoteId": string;
|
||||
"copyFileId": string;
|
||||
@ -114,7 +115,6 @@ export interface Locale {
|
||||
"renoted": string;
|
||||
"quoted": string;
|
||||
"rmboost": string;
|
||||
"rmquote": string;
|
||||
"cantRenote": string;
|
||||
"cantReRenote": string;
|
||||
"quote": string;
|
||||
@ -894,6 +894,7 @@ export interface Locale {
|
||||
"deleteAccountConfirm": string;
|
||||
"incorrectPassword": string;
|
||||
"voteConfirm": string;
|
||||
"voteConfirmMulti": string;
|
||||
"hide": string;
|
||||
"useDrawerReactionPickerForMobile": string;
|
||||
"welcomeBackWithName": string;
|
||||
@ -990,6 +991,7 @@ export interface Locale {
|
||||
"numberOfProfileView": string;
|
||||
"like": string;
|
||||
"unlike": string;
|
||||
"defaultLike": string;
|
||||
"numberOfLikes": string;
|
||||
"show": string;
|
||||
"neverShow": string;
|
||||
@ -2125,6 +2127,7 @@ export interface Locale {
|
||||
"chooseList": string;
|
||||
};
|
||||
"clicker": string;
|
||||
"search": string;
|
||||
};
|
||||
"_cw": {
|
||||
"hide": string;
|
||||
@ -2154,6 +2157,7 @@ export interface Locale {
|
||||
"remainingHours": string;
|
||||
"remainingMinutes": string;
|
||||
"remainingSeconds": string;
|
||||
"multiple": string;
|
||||
};
|
||||
"_visibility": {
|
||||
"public": string;
|
||||
|
@ -54,6 +54,7 @@ addToAntenna: "Aggiungi all'antenna"
|
||||
sendMessage: "Invia messaggio"
|
||||
copyRSS: "Copia RSS"
|
||||
copyUsername: "Copia nome utente"
|
||||
openRemoteProfile: "Apri profilo remoto"
|
||||
copyUserId: "Copia ID del profilo"
|
||||
copyNoteId: "Copia ID della Nota"
|
||||
copyFileId: "Copia ID del file"
|
||||
@ -731,6 +732,8 @@ thisIsExperimentalFeature: "Questa è una funzionalità sperimentale. Potrebbe e
|
||||
developer: "Sviluppatore"
|
||||
makeExplorable: "Profilo visibile pubblicamente nella pagina \"Esplora\""
|
||||
makeExplorableDescription: "Disabilitando questa opzione, il tuo profilo non verrà elencato nella pagina \"Esplora\"."
|
||||
makeIndexable: "Non indicizzare le note pubbliche"
|
||||
makeIndexableDescription: "Le tue note pubbliche non saranno cercabili"
|
||||
showGapBetweenNotesInTimeline: "Mostrare un intervallo tra le note sulla timeline"
|
||||
duplicate: "Duplica"
|
||||
left: "Sinistra"
|
||||
@ -964,6 +967,7 @@ cannotLoad: "Caricamento impossibile"
|
||||
numberOfProfileView: "Visualizzazioni profilo"
|
||||
like: "Mi piace!"
|
||||
unlike: "Non mi piace"
|
||||
defaultLike: "Emoji predefinita per \"mi piace\""
|
||||
numberOfLikes: "Numero di Like"
|
||||
show: "Visualizza"
|
||||
neverShow: "Non mostrare più"
|
||||
@ -1266,6 +1270,8 @@ _serverSettings:
|
||||
shortName: "Abbreviazione"
|
||||
shortNameDescription: "Un'abbreviazione o un nome comune che può essere visualizzato al posto del nome ufficiale lungo del server."
|
||||
fanoutTimelineDescription: "Attivando questa funzionalità migliori notevolmente la capacità delle Timeline di collezionare Note, riducendo il carico sul database. Tuttavia, aumenterà l'impiego di memoria RAM per Redis. Disattiva se il tuo server ha poca RAM o la funzionalità è irregolare."
|
||||
fanoutTimelineDbFallback: "Ripiega sul database"
|
||||
fanoutTimelineDbFallbackDescription: "Attivando questa funzionalità, nel caso che il contenuto di una Timeline non sia presente nella cache, verrà consultato il database. Disattivandola, il carico sul database sarà ulteriormente ridotto, ma le Timeline saranno limitate"
|
||||
_accountMigration:
|
||||
moveFrom: "Migra un altro profilo dentro a questo"
|
||||
moveFromSub: "Crea un alias verso un altro profilo remoto"
|
||||
@ -1702,6 +1708,7 @@ _serverDisconnectedBehavior:
|
||||
reload: "Ricarica automaticamente"
|
||||
dialog: "Apri avviso in finestra"
|
||||
quiet: "Visualizza avviso in modo discreto"
|
||||
disabled: "Non visualizzare l'avviso"
|
||||
_channel:
|
||||
create: "Nuovo canale"
|
||||
edit: "Gerisci canale"
|
||||
@ -1817,6 +1824,14 @@ _ago:
|
||||
monthsAgo: "{n} mesi fa"
|
||||
yearsAgo: "{n} anni fa"
|
||||
invalid: "Niente da visualizzare"
|
||||
_timeIn:
|
||||
seconds: "fra {n} secondi"
|
||||
minutes: "fra {n} minuti"
|
||||
hours: "fra {n} ore"
|
||||
days: "fra {n} giorni"
|
||||
weeks: "fra {n} settimane"
|
||||
months: "fra {n} mesi"
|
||||
years: "fra {n} anni"
|
||||
_time:
|
||||
second: "s"
|
||||
minute: "min"
|
||||
|
@ -55,6 +55,7 @@ addToAntenna: "アンテナに追加"
|
||||
sendMessage: "メッセージを送信"
|
||||
copyRSS: "RSSをコピー"
|
||||
copyUsername: "ユーザー名をコピー"
|
||||
openRemoteProfile: "リモートプロファイルを開く"
|
||||
copyUserId: "ユーザーIDをコピー"
|
||||
copyNoteId: "ノートIDをコピー"
|
||||
copyFileId: "ファイルIDをコピー"
|
||||
@ -111,7 +112,6 @@ unrenote: "リノート解除"
|
||||
renoted: "ブースト。"
|
||||
quoted: "引用。"
|
||||
rmboost: "アンブースト。"
|
||||
rmquote: "引用を削除しました。"
|
||||
cantRenote: "この投稿はリノートできません。"
|
||||
cantReRenote: "リノートをリノートすることはできません。"
|
||||
quote: "引用"
|
||||
@ -891,6 +891,7 @@ continueThread: "さらにスレッドを見る"
|
||||
deleteAccountConfirm: "アカウントが削除されます。よろしいですか?"
|
||||
incorrectPassword: "パスワードが間違っています。"
|
||||
voteConfirm: "「{choice}」に投票しますか?"
|
||||
voteConfirmMulti: "「{choice}」に投票しますか?\n 確認後、選択肢を増やすことができます。"
|
||||
hide: "隠す"
|
||||
useDrawerReactionPickerForMobile: "モバイルデバイスのときドロワーで表示"
|
||||
welcomeBackWithName: "おかえりなさい、{name}さん"
|
||||
@ -987,6 +988,7 @@ cannotLoad: "読み込めません"
|
||||
numberOfProfileView: "プロフィール表示回数"
|
||||
like: "いいね!"
|
||||
unlike: "いいねを解除"
|
||||
defaultLike: "絵文字のようなデフォルト"
|
||||
numberOfLikes: "いいね数"
|
||||
show: "表示"
|
||||
neverShow: "今後表示しない"
|
||||
@ -2029,6 +2031,7 @@ _widgets:
|
||||
_userList:
|
||||
chooseList: "リストを選択"
|
||||
clicker: "クリッカー"
|
||||
search: "検索"
|
||||
|
||||
_cw:
|
||||
hide: "隠す"
|
||||
@ -2058,6 +2061,7 @@ _poll:
|
||||
remainingHours: "終了まであと{h}時間{m}分"
|
||||
remainingMinutes: "終了まであと{m}分{s}秒"
|
||||
remainingSeconds: "終了まであと{s}秒"
|
||||
multiple: "複数の選択肢"
|
||||
|
||||
_visibility:
|
||||
public: "パブリック"
|
||||
@ -2403,7 +2407,7 @@ _externalResourceInstaller:
|
||||
_themeInstallFailed:
|
||||
title: "テーマのインストールに失敗しました"
|
||||
description: "テーマのインストール中に問題が発生しました。もう一度お試しください。エラーの詳細はJavascriptコンソールをご覧ください。"
|
||||
|
||||
|
||||
_animatedMFM:
|
||||
play: "MFMアニメーションを再生"
|
||||
stop: "MFMアニメーション停止"
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "sharkey",
|
||||
"version": "2023.11.1",
|
||||
"version": "2023.11.2",
|
||||
"codename": "shonk",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
@ -12,7 +12,7 @@ import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import type { MiDriveFile } from '@/models/DriveFile.js';
|
||||
import type { MiEmoji } from '@/models/Emoji.js';
|
||||
import type { EmojisRepository, MiRole, MiUser } from '@/models/_.js';
|
||||
import type { DriveFilesRepository, EmojisRepository, MiRole, MiUser } from '@/models/_.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { MemoryKVCache, RedisSingleCache } from '@/misc/cache.js';
|
||||
import { UtilityService } from '@/core/UtilityService.js';
|
||||
@ -20,6 +20,7 @@ import { query } from '@/misc/prelude/url.js';
|
||||
import type { Serialized } from '@/types.js';
|
||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import { DriveService } from './DriveService.js';
|
||||
|
||||
const parseEmojiStrRegexp = /^(\w+)(?:@([\w.-]+))?$/;
|
||||
|
||||
@ -38,11 +39,15 @@ export class CustomEmojiService implements OnApplicationShutdown {
|
||||
@Inject(DI.emojisRepository)
|
||||
private emojisRepository: EmojisRepository,
|
||||
|
||||
@Inject(DI.driveFilesRepository)
|
||||
private driveFilesRepository: DriveFilesRepository,
|
||||
|
||||
private utilityService: UtilityService,
|
||||
private idService: IdService,
|
||||
private emojiEntityService: EmojiEntityService,
|
||||
private moderationLogService: ModerationLogService,
|
||||
private globalEventService: GlobalEventService,
|
||||
private driveService: DriveService,
|
||||
) {
|
||||
this.cache = new MemoryKVCache<MiEmoji | null>(1000 * 60 * 60 * 12);
|
||||
|
||||
@ -259,6 +264,12 @@ export class CustomEmojiService implements OnApplicationShutdown {
|
||||
|
||||
this.localEmojisCache.refresh();
|
||||
|
||||
const file = await this.driveFilesRepository.findOneBy({ url: emoji.originalUrl, userHost: emoji.host ? emoji.host : IsNull() });
|
||||
|
||||
if (file) {
|
||||
await this.driveService.deleteFile(file, false, moderator ? moderator : undefined);
|
||||
}
|
||||
|
||||
this.globalEventService.publishBroadcastStream('emojiDeleted', {
|
||||
emojis: [await this.emojiEntityService.packDetailed(emoji)],
|
||||
});
|
||||
@ -280,6 +291,12 @@ export class CustomEmojiService implements OnApplicationShutdown {
|
||||
for (const emoji of emojis) {
|
||||
await this.emojisRepository.delete(emoji.id);
|
||||
|
||||
const file = await this.driveFilesRepository.findOneBy({ url: emoji.originalUrl, userHost: emoji.host ? emoji.host : IsNull() });
|
||||
|
||||
if (file) {
|
||||
await this.driveService.deleteFile(file, false, moderator ? moderator : undefined);
|
||||
}
|
||||
|
||||
if (moderator) {
|
||||
this.moderationLogService.log(moderator, 'deleteCustomEmoji', {
|
||||
emojiId: emoji.id,
|
||||
|
@ -14,18 +14,15 @@ import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mf
|
||||
import { extractHashtags } from '@/misc/extract-hashtags.js';
|
||||
import type { IMentionedRemoteUsers } from '@/models/Note.js';
|
||||
import { MiNote } from '@/models/Note.js';
|
||||
import type { NoteEditRepository, ChannelFollowingsRepository, ChannelsRepository, FollowingsRepository, InstancesRepository, MiFollowing, MutingsRepository, NotesRepository, NoteThreadMutingsRepository, UserListMembershipsRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js';
|
||||
import type { NoteEditRepository, ChannelFollowingsRepository, ChannelsRepository, FollowingsRepository, InstancesRepository, MiFollowing, MutingsRepository, NotesRepository, NoteThreadMutingsRepository, UserListMembershipsRepository, UserProfilesRepository, UsersRepository, PollsRepository } from '@/models/_.js';
|
||||
import type { MiDriveFile } from '@/models/DriveFile.js';
|
||||
import type { MiApp } from '@/models/App.js';
|
||||
import { concat } from '@/misc/prelude/array.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import type { MiUser, MiLocalUser, MiRemoteUser } from '@/models/User.js';
|
||||
import { MiPoll, type IPoll } from '@/models/Poll.js';
|
||||
import { checkWordMute } from '@/misc/check-word-mute.js';
|
||||
import type { MiChannel } from '@/models/Channel.js';
|
||||
import { normalizeForSearch } from '@/misc/normalize-for-search.js';
|
||||
import { MemorySingleCache } from '@/misc/cache.js';
|
||||
import type { MiUserProfile } from '@/models/UserProfile.js';
|
||||
import { RelayService } from '@/core/RelayService.js';
|
||||
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
@ -35,7 +32,6 @@ import ActiveUsersChart from '@/core/chart/charts/active-users.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { NotificationService } from '@/core/NotificationService.js';
|
||||
import { WebhookService } from '@/core/WebhookService.js';
|
||||
import { HashtagService } from '@/core/HashtagService.js';
|
||||
import { QueueService } from '@/core/QueueService.js';
|
||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
@ -48,11 +44,7 @@ import { DB_MAX_NOTE_TEXT_LENGTH } from '@/const.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
import { SearchService } from '@/core/SearchService.js';
|
||||
import { FeaturedService } from '@/core/FeaturedService.js';
|
||||
import { FunoutTimelineService } from '@/core/FunoutTimelineService.js';
|
||||
import { AntennaService } from './AntennaService.js';
|
||||
import NotesChart from './chart/charts/notes.js';
|
||||
import PerUserNotesChart from './chart/charts/per-user-notes.js';
|
||||
import { UtilityService } from '@/core/UtilityService.js';
|
||||
|
||||
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
|
||||
@ -191,6 +183,9 @@ export class NoteEditService implements OnApplicationShutdown {
|
||||
@Inject(DI.noteEditRepository)
|
||||
private noteEditRepository: NoteEditRepository,
|
||||
|
||||
@Inject(DI.pollsRepository)
|
||||
private pollsRepository: PollsRepository,
|
||||
|
||||
private userEntityService: UserEntityService,
|
||||
private noteEntityService: NoteEntityService,
|
||||
private idService: IdService,
|
||||
@ -201,18 +196,13 @@ export class NoteEditService implements OnApplicationShutdown {
|
||||
private notificationService: NotificationService,
|
||||
private relayService: RelayService,
|
||||
private federatedInstanceService: FederatedInstanceService,
|
||||
private hashtagService: HashtagService,
|
||||
private antennaService: AntennaService,
|
||||
private webhookService: WebhookService,
|
||||
private featuredService: FeaturedService,
|
||||
private remoteUserResolveService: RemoteUserResolveService,
|
||||
private apDeliverManagerService: ApDeliverManagerService,
|
||||
private apRendererService: ApRendererService,
|
||||
private roleService: RoleService,
|
||||
private metaService: MetaService,
|
||||
private searchService: SearchService,
|
||||
private notesChart: NotesChart,
|
||||
private perUserNotesChart: PerUserNotesChart,
|
||||
private activeUsersChart: ActiveUsersChart,
|
||||
private instanceChart: InstanceChart,
|
||||
private utilityService: UtilityService,
|
||||
@ -385,6 +375,10 @@ export class NoteEditService implements OnApplicationShutdown {
|
||||
update.hasPoll = !!data.poll;
|
||||
}
|
||||
|
||||
const poll = await this.pollsRepository.findOneBy({ noteId: oldnote.id });
|
||||
|
||||
const oldPoll = poll ? { choices: poll.choices, multiple: poll.multiple, expiresAt: poll.expiresAt } : null;
|
||||
|
||||
if (Object.keys(update).length > 0) {
|
||||
const exists = await this.noteEditRepository.findOneBy({ noteId: oldnote.id });
|
||||
|
||||
@ -456,7 +450,7 @@ export class NoteEditService implements OnApplicationShutdown {
|
||||
}));
|
||||
}
|
||||
|
||||
if (data.poll != null) {
|
||||
if (data.poll != null && JSON.stringify(data.poll) !== JSON.stringify(oldPoll)) {
|
||||
// Start transaction
|
||||
await this.db.transaction(async transactionalEntityManager => {
|
||||
await transactionalEntityManager.update(MiNote, oldnote.id, note);
|
||||
|
@ -278,14 +278,14 @@ export class QueueService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public createImportMastoToDbJob(user: ThinUser, targets: string[]) {
|
||||
const jobs = targets.map(rel => this.generateToDbJobData('importMastoToDb', { user, target: rel }));
|
||||
public createImportMastoToDbJob(user: ThinUser, targets: string[], note: MiNote['id'] | null) {
|
||||
const jobs = targets.map(rel => this.generateToDbJobData('importMastoToDb', { user, target: rel, note }));
|
||||
return this.dbQueue.addBulk(jobs);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public createImportPleroToDbJob(user: ThinUser, targets: string[]) {
|
||||
const jobs = targets.map(rel => this.generateToDbJobData('importPleroToDb', { user, target: rel }));
|
||||
public createImportPleroToDbJob(user: ThinUser, targets: string[], note: MiNote['id'] | null) {
|
||||
const jobs = targets.map(rel => this.generateToDbJobData('importPleroToDb', { user, target: rel, note }));
|
||||
return this.dbQueue.addBulk(jobs);
|
||||
}
|
||||
|
||||
|
@ -171,6 +171,7 @@ export class ApRendererService {
|
||||
mediaType: file.webpublicType ?? file.type,
|
||||
url: this.driveFileEntityService.getPublicUrl(file),
|
||||
name: file.comment,
|
||||
summary: file.comment,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -11,7 +11,7 @@ export interface IObject {
|
||||
type: string | string[];
|
||||
id?: string;
|
||||
name?: string | null;
|
||||
summary?: string;
|
||||
summary?: string | null;
|
||||
_misskey_summary?: string;
|
||||
published?: string;
|
||||
cc?: ApObject;
|
||||
|
@ -3,7 +3,7 @@ import * as vm from 'node:vm';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { ZipReader } from 'slacc';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { UsersRepository, DriveFilesRepository, MiDriveFile, MiNote, NotesRepository, MiUser } from '@/models/_.js';
|
||||
import type { UsersRepository, DriveFilesRepository, MiDriveFile, MiNote, NotesRepository, MiUser, DriveFoldersRepository, MiDriveFolder } from '@/models/_.js';
|
||||
import type Logger from '@/logger.js';
|
||||
import { DownloadService } from '@/core/DownloadService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
@ -14,9 +14,10 @@ import { DriveService } from '@/core/DriveService.js';
|
||||
import { MfmService } from '@/core/MfmService.js';
|
||||
import { ApNoteService } from '@/core/activitypub/models/ApNoteService.js';
|
||||
import { extractApHashtagObjects } from '@/core/activitypub/models/tag.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||
import type * as Bull from 'bullmq';
|
||||
import type { DbNoteImportToDbJobData, DbNoteImportJobData, DbKeyNoteImportToDbJobData } from '../types.js';
|
||||
import type { DbNoteImportToDbJobData, DbNoteImportJobData, DbNoteWithParentImportToDbJobData } from '../types.js';
|
||||
|
||||
@Injectable()
|
||||
export class ImportNotesProcessorService {
|
||||
@ -29,6 +30,9 @@ export class ImportNotesProcessorService {
|
||||
@Inject(DI.driveFilesRepository)
|
||||
private driveFilesRepository: DriveFilesRepository,
|
||||
|
||||
@Inject(DI.driveFoldersRepository)
|
||||
private driveFoldersRepository: DriveFoldersRepository,
|
||||
|
||||
@Inject(DI.notesRepository)
|
||||
private notesRepository: NotesRepository,
|
||||
|
||||
@ -38,20 +42,21 @@ export class ImportNotesProcessorService {
|
||||
private apNoteService: ApNoteService,
|
||||
private driveService: DriveService,
|
||||
private downloadService: DownloadService,
|
||||
private idService: IdService,
|
||||
private queueLoggerService: QueueLoggerService,
|
||||
) {
|
||||
this.logger = this.queueLoggerService.logger.createSubLogger('import-notes');
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async uploadFiles(dir: string, user: MiUser) {
|
||||
private async uploadFiles(dir: string, user: MiUser, folder?: MiDriveFolder['id']) {
|
||||
const fileList = fs.readdirSync(dir);
|
||||
for await (const file of fileList) {
|
||||
const name = `${dir}/${file}`;
|
||||
if (fs.statSync(name).isDirectory()) {
|
||||
await this.uploadFiles(name, user);
|
||||
await this.uploadFiles(name, user, folder);
|
||||
} else {
|
||||
const exists = await this.driveFilesRepository.findOneBy({ name: file, userId: user.id });
|
||||
const exists = await this.driveFilesRepository.findOneBy({ name: file, userId: user.id, folderId: folder });
|
||||
|
||||
if (file.endsWith('.srt')) return;
|
||||
|
||||
@ -60,6 +65,7 @@ export class ImportNotesProcessorService {
|
||||
user: user,
|
||||
path: name,
|
||||
name: file,
|
||||
folderId: folder,
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -68,7 +74,7 @@ export class ImportNotesProcessorService {
|
||||
|
||||
// Function was taken from Firefish and modified for our needs
|
||||
@bindThis
|
||||
private async recreateChain(idField: string, replyField: string, arr: any[]): Promise<any[]> {
|
||||
private async recreateChain(idFieldPath: string[], replyFieldPath: string[], arr: any[], includeOrphans: boolean): Promise<any[]> {
|
||||
type NotesMap = {
|
||||
[id: string]: any;
|
||||
};
|
||||
@ -77,28 +83,42 @@ export class ImportNotesProcessorService {
|
||||
const notesWaitingForParent: NotesMap = {};
|
||||
|
||||
for await (const note of arr) {
|
||||
noteById[note[idField]] = note;
|
||||
const noteId = idFieldPath.reduce(
|
||||
(obj, step) => obj[step],
|
||||
note,
|
||||
);
|
||||
|
||||
noteById[noteId] = note;
|
||||
note.childNotes = [];
|
||||
|
||||
const children = notesWaitingForParent[note[idField]];
|
||||
const children = notesWaitingForParent[noteId];
|
||||
if (children) {
|
||||
note.childNotes.push(...children);
|
||||
delete notesWaitingForParent[noteId];
|
||||
}
|
||||
|
||||
if (note[replyField] == null) {
|
||||
const noteReplyId = replyFieldPath.reduce(
|
||||
(obj, step) => obj[step],
|
||||
note,
|
||||
);
|
||||
if (noteReplyId == null) {
|
||||
notesTree.push(note);
|
||||
continue;
|
||||
}
|
||||
|
||||
const parent = noteById[note[replyField]];
|
||||
const parent = noteById[noteReplyId];
|
||||
if (parent) {
|
||||
parent.childNotes.push(note);
|
||||
} else {
|
||||
notesWaitingForParent[note[replyField]] ||= [];
|
||||
notesWaitingForParent[note[replyField]].push(note);
|
||||
notesWaitingForParent[noteReplyId] ||= [];
|
||||
notesWaitingForParent[noteReplyId].push(note);
|
||||
}
|
||||
}
|
||||
|
||||
if (includeOrphans) {
|
||||
notesTree.push(...Object.values(notesWaitingForParent).flat(1));
|
||||
}
|
||||
|
||||
return notesTree;
|
||||
}
|
||||
|
||||
@ -126,6 +146,12 @@ export class ImportNotesProcessorService {
|
||||
return;
|
||||
}
|
||||
|
||||
let folder = await this.driveFoldersRepository.findOneBy({ name: 'Imports', userId: job.data.user.id });
|
||||
if (folder == null) {
|
||||
await this.driveFoldersRepository.insert({ id: this.idService.gen(), name: 'Imports', userId: job.data.user.id });
|
||||
folder = await this.driveFoldersRepository.findOneBy({ name: 'Imports', userId: job.data.user.id });
|
||||
}
|
||||
|
||||
const type = job.data.type;
|
||||
|
||||
if (type === 'Twitter' || file.name.startsWith('twitter') && file.name.endsWith('.zip')) {
|
||||
@ -164,7 +190,7 @@ export class ImportNotesProcessorService {
|
||||
const tweets = Object.keys(fakeWindow.window.YTD.tweets.part0).reduce((m, key, i, obj) => {
|
||||
return m.concat(fakeWindow.window.YTD.tweets.part0[key].tweet);
|
||||
}, []);
|
||||
const processedTweets = await this.recreateChain("id_str", "in_reply_to_status_id_str", tweets);
|
||||
const processedTweets = await this.recreateChain(['id_str'], ['in_reply_to_status_id_str'], tweets, false);
|
||||
this.queueService.createImportTweetsToDbJob(job.data.user, processedTweets, null);
|
||||
} finally {
|
||||
cleanup();
|
||||
@ -192,7 +218,12 @@ export class ImportNotesProcessorService {
|
||||
ZipReader.withDestinationPath(outputPath).viaBuffer(await fs.promises.readFile(destPath));
|
||||
const postsJson = fs.readFileSync(outputPath + '/your_activity_across_facebook/posts/your_posts__check_ins__photos_and_videos_1.json', 'utf-8');
|
||||
const posts = JSON.parse(postsJson);
|
||||
await this.uploadFiles(outputPath + '/your_activity_across_facebook/posts/media', user);
|
||||
const facebookFolder = await this.driveFoldersRepository.findOneBy({ name: 'Facebook', userId: job.data.user.id, parentId: folder?.id });
|
||||
if (facebookFolder == null && folder) {
|
||||
await this.driveFoldersRepository.insert({ id: this.idService.gen(), name: 'Facebook', userId: job.data.user.id, parentId: folder.id });
|
||||
const createdFolder = await this.driveFoldersRepository.findOneBy({ name: 'Facebook', userId: job.data.user.id, parentId: folder.id });
|
||||
if (createdFolder) await this.uploadFiles(outputPath + '/your_activity_across_facebook/posts/media', user, createdFolder.id);
|
||||
}
|
||||
this.queueService.createImportFBToDbJob(job.data.user, posts);
|
||||
} finally {
|
||||
cleanup();
|
||||
@ -223,7 +254,12 @@ export class ImportNotesProcessorService {
|
||||
if (isInstagram) {
|
||||
const postsJson = fs.readFileSync(outputPath + '/content/posts_1.json', 'utf-8');
|
||||
const posts = JSON.parse(postsJson);
|
||||
await this.uploadFiles(outputPath + '/media/posts', user);
|
||||
const igFolder = await this.driveFoldersRepository.findOneBy({ name: 'Instagram', userId: job.data.user.id, parentId: folder?.id });
|
||||
if (igFolder == null && folder) {
|
||||
await this.driveFoldersRepository.insert({ id: this.idService.gen(), name: 'Instagram', userId: job.data.user.id, parentId: folder.id });
|
||||
const createdFolder = await this.driveFoldersRepository.findOneBy({ name: 'Instagram', userId: job.data.user.id, parentId: folder.id });
|
||||
if (createdFolder) await this.uploadFiles(outputPath + '/media/posts', user, createdFolder.id);
|
||||
}
|
||||
this.queueService.createImportIGToDbJob(job.data.user, posts);
|
||||
} else if (isOutbox) {
|
||||
const actorJson = fs.readFileSync(outputPath + '/actor.json', 'utf-8');
|
||||
@ -232,12 +268,21 @@ export class ImportNotesProcessorService {
|
||||
if (isPleroma) {
|
||||
const outboxJson = fs.readFileSync(outputPath + '/outbox.json', 'utf-8');
|
||||
const outbox = JSON.parse(outboxJson);
|
||||
this.queueService.createImportPleroToDbJob(job.data.user, outbox.orderedItems.filter((x: any) => x.type === 'Create' && x.object.type === 'Note'));
|
||||
const processedToots = await this.recreateChain(['object', 'id'], ['object', 'inReplyTo'], outbox.orderedItems.filter((x: any) => x.type === 'Create' && x.object.type === 'Note'), true);
|
||||
this.queueService.createImportPleroToDbJob(job.data.user, processedToots, null);
|
||||
} else {
|
||||
const outboxJson = fs.readFileSync(outputPath + '/outbox.json', 'utf-8');
|
||||
const outbox = JSON.parse(outboxJson);
|
||||
if (fs.existsSync(outputPath + '/media_attachments/files')) await this.uploadFiles(outputPath + '/media_attachments/files', user);
|
||||
this.queueService.createImportMastoToDbJob(job.data.user, outbox.orderedItems.filter((x: any) => x.type === 'Create' && x.object.type === 'Note'));
|
||||
let mastoFolder = await this.driveFoldersRepository.findOneBy({ name: 'Mastodon', userId: job.data.user.id, parentId: folder?.id });
|
||||
if (mastoFolder == null && folder) {
|
||||
await this.driveFoldersRepository.insert({ id: this.idService.gen(), name: 'Mastodon', userId: job.data.user.id, parentId: folder.id });
|
||||
mastoFolder = await this.driveFoldersRepository.findOneBy({ name: 'Mastodon', userId: job.data.user.id, parentId: folder.id });
|
||||
}
|
||||
if (fs.existsSync(outputPath + '/media_attachments/files') && mastoFolder) {
|
||||
await this.uploadFiles(outputPath + '/media_attachments/files', user, mastoFolder.id);
|
||||
}
|
||||
const processedToots = await this.recreateChain(['object', 'id'], ['object', 'inReplyTo'], outbox.orderedItems.filter((x: any) => x.type === 'Create' && x.object.type === 'Note'), true);
|
||||
this.queueService.createImportMastoToDbJob(job.data.user, processedToots, null);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
@ -260,7 +305,7 @@ export class ImportNotesProcessorService {
|
||||
|
||||
const notesJson = fs.readFileSync(path, 'utf-8');
|
||||
const notes = JSON.parse(notesJson);
|
||||
const processedNotes = await this.recreateChain("id", "replyId", notes);
|
||||
const processedNotes = await this.recreateChain(['id'], ['replyId'], notes, false);
|
||||
this.queueService.createImportKeyNotesToDbJob(job.data.user, processedNotes, null);
|
||||
cleanup();
|
||||
}
|
||||
@ -269,7 +314,7 @@ export class ImportNotesProcessorService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async processKeyNotesToDb(job: Bull.Job<DbKeyNoteImportToDbJobData>): Promise<void> {
|
||||
public async processKeyNotesToDb(job: Bull.Job<DbNoteWithParentImportToDbJobData>): Promise<void> {
|
||||
const note = job.data.target;
|
||||
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
|
||||
if (user == null) {
|
||||
@ -280,16 +325,25 @@ export class ImportNotesProcessorService {
|
||||
|
||||
const parentNote = job.data.note ? await this.notesRepository.findOneBy({ id: job.data.note }) : null;
|
||||
|
||||
const folder = await this.driveFoldersRepository.findOneBy({ name: 'Imports', userId: job.data.user.id });
|
||||
if (folder == null) return;
|
||||
|
||||
const files: MiDriveFile[] = [];
|
||||
const date = new Date(note.createdAt);
|
||||
|
||||
if (note.files && this.isIterable(note.files)) {
|
||||
let keyFolder = await this.driveFoldersRepository.findOneBy({ name: 'Misskey', userId: job.data.user.id, parentId: folder.id });
|
||||
if (keyFolder == null) {
|
||||
await this.driveFoldersRepository.insert({ id: this.idService.gen(), name: 'Misskey', userId: job.data.user.id, parentId: folder.id });
|
||||
keyFolder = await this.driveFoldersRepository.findOneBy({ name: 'Misskey', userId: job.data.user.id, parentId: folder.id });
|
||||
}
|
||||
|
||||
for await (const file of note.files) {
|
||||
const [filePath, cleanup] = await createTemp();
|
||||
const slashdex = file.url.lastIndexOf('/');
|
||||
const name = file.url.substring(slashdex + 1);
|
||||
|
||||
const exists = await this.driveFilesRepository.findOneBy({ name: name, userId: user.id });
|
||||
const exists = await this.driveFilesRepository.findOneBy({ name: name, userId: user.id }) ?? await this.driveFilesRepository.findOneBy({ name: name, userId: user.id, folderId: keyFolder?.id });
|
||||
|
||||
if (!exists) {
|
||||
try {
|
||||
@ -301,6 +355,7 @@ export class ImportNotesProcessorService {
|
||||
user: user,
|
||||
path: filePath,
|
||||
name: name,
|
||||
folderId: keyFolder?.id,
|
||||
});
|
||||
files.push(driveFile);
|
||||
} else {
|
||||
@ -316,28 +371,33 @@ export class ImportNotesProcessorService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async processMastoToDb(job: Bull.Job<DbNoteImportToDbJobData>): Promise<void> {
|
||||
public async processMastoToDb(job: Bull.Job<DbNoteWithParentImportToDbJobData>): Promise<void> {
|
||||
const toot = job.data.target;
|
||||
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
|
||||
if (user == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (toot.directMessage) return;
|
||||
|
||||
const date = new Date(toot.object.published);
|
||||
let text = undefined;
|
||||
const files: MiDriveFile[] = [];
|
||||
let reply: MiNote | null = null;
|
||||
|
||||
if (toot.object.inReplyTo != null) {
|
||||
try {
|
||||
reply = await this.apNoteService.resolveNote(toot.object.inReplyTo);
|
||||
} catch (error) {
|
||||
reply = null;
|
||||
const parentNote = job.data.note ? await this.notesRepository.findOneBy({ id: job.data.note }) : null;
|
||||
if (parentNote) {
|
||||
reply = parentNote;
|
||||
} else {
|
||||
try {
|
||||
reply = await this.apNoteService.resolveNote(toot.object.inReplyTo);
|
||||
} catch (error) {
|
||||
reply = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (toot.directMessage) return;
|
||||
|
||||
const hashtags = extractApHashtagObjects(toot.object.tag).map((x) => x.name).filter((x): x is string => x != null);
|
||||
|
||||
try {
|
||||
@ -357,32 +417,41 @@ export class ImportNotesProcessorService {
|
||||
}
|
||||
}
|
||||
|
||||
await this.noteCreateService.import(user, { createdAt: date, text: text, files: files, apMentions: new Array(0), cw: toot.object.sensitive ? toot.object.summary : null, reply: reply });
|
||||
const createdNote = await this.noteCreateService.import(user, { createdAt: date, text: text, files: files, apMentions: new Array(0), cw: toot.object.sensitive ? toot.object.summary : null, reply: reply });
|
||||
if (toot.childNotes) this.queueService.createImportMastoToDbJob(user, toot.childNotes, createdNote.id);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async processPleroToDb(job: Bull.Job<DbNoteImportToDbJobData>): Promise<void> {
|
||||
public async processPleroToDb(job: Bull.Job<DbNoteWithParentImportToDbJobData>): Promise<void> {
|
||||
const post = job.data.target;
|
||||
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
|
||||
if (user == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (post.directMessage) return;
|
||||
|
||||
const date = new Date(post.object.published);
|
||||
let text = undefined;
|
||||
const files: MiDriveFile[] = [];
|
||||
let reply: MiNote | null = null;
|
||||
|
||||
const folder = await this.driveFoldersRepository.findOneBy({ name: 'Imports', userId: job.data.user.id });
|
||||
if (folder == null) return;
|
||||
|
||||
if (post.object.inReplyTo != null) {
|
||||
try {
|
||||
reply = await this.apNoteService.resolveNote(post.object.inReplyTo);
|
||||
} catch (error) {
|
||||
reply = null;
|
||||
const parentNote = job.data.note ? await this.notesRepository.findOneBy({ id: job.data.note }) : null;
|
||||
if (parentNote) {
|
||||
reply = parentNote;
|
||||
} else {
|
||||
try {
|
||||
reply = await this.apNoteService.resolveNote(post.object.inReplyTo);
|
||||
} catch (error) {
|
||||
reply = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (post.directMessage) return;
|
||||
|
||||
const hashtags = extractApHashtagObjects(post.object.tag).map((x) => x.name).filter((x): x is string => x != null);
|
||||
|
||||
try {
|
||||
@ -392,12 +461,18 @@ export class ImportNotesProcessorService {
|
||||
}
|
||||
|
||||
if (post.object.attachment && this.isIterable(post.object.attachment)) {
|
||||
let pleroFolder = await this.driveFoldersRepository.findOneBy({ name: 'Pleroma', userId: job.data.user.id, parentId: folder.id });
|
||||
if (pleroFolder == null) {
|
||||
await this.driveFoldersRepository.insert({ id: this.idService.gen(), name: 'Pleroma', userId: job.data.user.id, parentId: folder.id });
|
||||
pleroFolder = await this.driveFoldersRepository.findOneBy({ name: 'Pleroma', userId: job.data.user.id, parentId: folder.id });
|
||||
}
|
||||
|
||||
for await (const file of post.object.attachment) {
|
||||
const slashdex = file.url.lastIndexOf('/');
|
||||
const name = file.url.substring(slashdex + 1);
|
||||
const [filePath, cleanup] = await createTemp();
|
||||
|
||||
const exists = await this.driveFilesRepository.findOneBy({ name: name, userId: user.id });
|
||||
const exists = await this.driveFilesRepository.findOneBy({ name: name, userId: user.id }) ?? await this.driveFilesRepository.findOneBy({ name: name, userId: user.id, folderId: pleroFolder?.id });
|
||||
|
||||
if (!exists) {
|
||||
try {
|
||||
@ -409,6 +484,7 @@ export class ImportNotesProcessorService {
|
||||
user: user,
|
||||
path: filePath,
|
||||
name: name,
|
||||
folderId: pleroFolder?.id,
|
||||
});
|
||||
files.push(driveFile);
|
||||
} else {
|
||||
@ -419,7 +495,8 @@ export class ImportNotesProcessorService {
|
||||
}
|
||||
}
|
||||
|
||||
await this.noteCreateService.import(user, { createdAt: date, text: text, files: files, apMentions: new Array(0), cw: post.object.sensitive ? post.object.summary : null, reply: reply });
|
||||
const createdNote = await this.noteCreateService.import(user, { createdAt: date, text: text, files: files, apMentions: new Array(0), cw: post.object.sensitive ? post.object.summary : null, reply: reply });
|
||||
if (post.childNotes) this.queueService.createImportPleroToDbJob(user, post.childNotes, createdNote.id);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
@ -468,13 +545,16 @@ export class ImportNotesProcessorService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async processTwitterDb(job: Bull.Job<DbKeyNoteImportToDbJobData>): Promise<void> {
|
||||
public async processTwitterDb(job: Bull.Job<DbNoteWithParentImportToDbJobData>): Promise<void> {
|
||||
const tweet = job.data.target;
|
||||
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
|
||||
if (user == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const folder = await this.driveFoldersRepository.findOneBy({ name: 'Imports', userId: job.data.user.id });
|
||||
if (folder == null) return;
|
||||
|
||||
const parentNote = job.data.note ? await this.notesRepository.findOneBy({ id: job.data.note }) : null;
|
||||
|
||||
async function replaceTwitterUrls(full_text: string, urls: any) {
|
||||
@ -500,13 +580,19 @@ export class ImportNotesProcessorService {
|
||||
const files: MiDriveFile[] = [];
|
||||
|
||||
if (tweet.extended_entities && this.isIterable(tweet.extended_entities.media)) {
|
||||
let twitFolder = await this.driveFoldersRepository.findOneBy({ name: 'Twitter', userId: job.data.user.id, parentId: folder.id });
|
||||
if (twitFolder == null) {
|
||||
await this.driveFoldersRepository.insert({ id: this.idService.gen(), name: 'Twitter', userId: job.data.user.id, parentId: folder.id });
|
||||
twitFolder = await this.driveFoldersRepository.findOneBy({ name: 'Twitter', userId: job.data.user.id, parentId: folder.id });
|
||||
}
|
||||
|
||||
for await (const file of tweet.extended_entities.media) {
|
||||
if (file.video_info) {
|
||||
const [filePath, cleanup] = await createTemp();
|
||||
const slashdex = file.video_info.variants[0].url.lastIndexOf('/');
|
||||
const name = file.video_info.variants[0].url.substring(slashdex + 1);
|
||||
|
||||
const exists = await this.driveFilesRepository.findOneBy({ name: name, userId: user.id });
|
||||
const exists = await this.driveFilesRepository.findOneBy({ name: name, userId: user.id }) ?? await this.driveFilesRepository.findOneBy({ name: name, userId: user.id, folderId: twitFolder?.id });
|
||||
|
||||
const videos = file.video_info.variants.filter((x: any) => x.content_type === 'video/mp4');
|
||||
|
||||
@ -520,6 +606,7 @@ export class ImportNotesProcessorService {
|
||||
user: user,
|
||||
path: filePath,
|
||||
name: name,
|
||||
folderId: twitFolder?.id,
|
||||
});
|
||||
files.push(driveFile);
|
||||
} else {
|
||||
@ -545,6 +632,7 @@ export class ImportNotesProcessorService {
|
||||
user: user,
|
||||
path: filePath,
|
||||
name: name,
|
||||
folderId: twitFolder?.id,
|
||||
});
|
||||
files.push(driveFile);
|
||||
} else {
|
||||
|
@ -50,12 +50,12 @@ export type DbJobMap = {
|
||||
exportUserLists: DbJobDataWithUser;
|
||||
importAntennas: DBAntennaImportJobData;
|
||||
importNotes: DbNoteImportJobData;
|
||||
importTweetsToDb: DbKeyNoteImportToDbJobData;
|
||||
importTweetsToDb: DbNoteWithParentImportToDbJobData;
|
||||
importIGToDb: DbNoteImportToDbJobData;
|
||||
importFBToDb: DbNoteImportToDbJobData;
|
||||
importMastoToDb: DbNoteImportToDbJobData;
|
||||
importPleroToDb: DbNoteImportToDbJobData;
|
||||
importKeyNotesToDb: DbKeyNoteImportToDbJobData;
|
||||
importMastoToDb: DbNoteWithParentImportToDbJobData;
|
||||
importPleroToDb: DbNoteWithParentImportToDbJobData;
|
||||
importKeyNotesToDb: DbNoteWithParentImportToDbJobData;
|
||||
importFollowing: DbUserImportJobData;
|
||||
importFollowingToDb: DbUserImportToDbJobData;
|
||||
importMuting: DbUserImportJobData;
|
||||
@ -113,7 +113,7 @@ export type DbNoteImportToDbJobData = {
|
||||
target: any;
|
||||
};
|
||||
|
||||
export type DbKeyNoteImportToDbJobData = {
|
||||
export type DbNoteWithParentImportToDbJobData = {
|
||||
user: ThinUser;
|
||||
target: any;
|
||||
note: MiNote['id'] | null;
|
||||
|
@ -13,7 +13,7 @@ export const meta = {
|
||||
prohibitMoved: true,
|
||||
limit: {
|
||||
duration: ms('1hour'),
|
||||
max: 5,
|
||||
max: 2,
|
||||
},
|
||||
|
||||
errors: {
|
||||
|
@ -14,7 +14,7 @@ import { DI } from '@/di-symbols.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ["notes"],
|
||||
tags: ['notes'],
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
@ -23,99 +23,99 @@ export const meta = {
|
||||
max: 300,
|
||||
},
|
||||
|
||||
kind: "write:notes",
|
||||
kind: 'write:notes',
|
||||
|
||||
res: {
|
||||
type: "object",
|
||||
type: 'object',
|
||||
optional: false,
|
||||
nullable: false,
|
||||
properties: {
|
||||
createdNote: {
|
||||
type: "object",
|
||||
type: 'object',
|
||||
optional: false,
|
||||
nullable: false,
|
||||
ref: "Note",
|
||||
ref: 'Note',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
errors: {
|
||||
noSuchRenoteTarget: {
|
||||
message: "No such renote target.",
|
||||
code: "NO_SUCH_RENOTE_TARGET",
|
||||
id: "b5c90186-4ab0-49c8-9bba-a1f76c282ba4",
|
||||
message: 'No such renote target.',
|
||||
code: 'NO_SUCH_RENOTE_TARGET',
|
||||
id: 'b5c90186-4ab0-49c8-9bba-a1f76c282ba4',
|
||||
},
|
||||
|
||||
cannotReRenote: {
|
||||
message: "You can not Renote a pure Renote.",
|
||||
code: "CANNOT_RENOTE_TO_A_PURE_RENOTE",
|
||||
id: "fd4cc33e-2a37-48dd-99cc-9b806eb2031a",
|
||||
message: 'You can not Renote a pure Renote.',
|
||||
code: 'CANNOT_RENOTE_TO_A_PURE_RENOTE',
|
||||
id: 'fd4cc33e-2a37-48dd-99cc-9b806eb2031a',
|
||||
},
|
||||
|
||||
noSuchReplyTarget: {
|
||||
message: "No such reply target.",
|
||||
code: "NO_SUCH_REPLY_TARGET",
|
||||
id: "749ee0f6-d3da-459a-bf02-282e2da4292c",
|
||||
message: 'No such reply target.',
|
||||
code: 'NO_SUCH_REPLY_TARGET',
|
||||
id: '749ee0f6-d3da-459a-bf02-282e2da4292c',
|
||||
},
|
||||
|
||||
cannotReplyToPureRenote: {
|
||||
message: "You can not reply to a pure Renote.",
|
||||
code: "CANNOT_REPLY_TO_A_PURE_RENOTE",
|
||||
id: "3ac74a84-8fd5-4bb0-870f-01804f82ce15",
|
||||
message: 'You can not reply to a pure Renote.',
|
||||
code: 'CANNOT_REPLY_TO_A_PURE_RENOTE',
|
||||
id: '3ac74a84-8fd5-4bb0-870f-01804f82ce15',
|
||||
},
|
||||
|
||||
cannotCreateAlreadyExpiredPoll: {
|
||||
message: "Poll is already expired.",
|
||||
code: "CANNOT_CREATE_ALREADY_EXPIRED_POLL",
|
||||
id: "04da457d-b083-4055-9082-955525eda5a5",
|
||||
message: 'Poll is already expired.',
|
||||
code: 'CANNOT_CREATE_ALREADY_EXPIRED_POLL',
|
||||
id: '04da457d-b083-4055-9082-955525eda5a5',
|
||||
},
|
||||
|
||||
noSuchChannel: {
|
||||
message: "No such channel.",
|
||||
code: "NO_SUCH_CHANNEL",
|
||||
id: "b1653923-5453-4edc-b786-7c4f39bb0bbb",
|
||||
message: 'No such channel.',
|
||||
code: 'NO_SUCH_CHANNEL',
|
||||
id: 'b1653923-5453-4edc-b786-7c4f39bb0bbb',
|
||||
},
|
||||
|
||||
youHaveBeenBlocked: {
|
||||
message: "You have been blocked by this user.",
|
||||
code: "YOU_HAVE_BEEN_BLOCKED",
|
||||
id: "b390d7e1-8a5e-46ed-b625-06271cafd3d3",
|
||||
message: 'You have been blocked by this user.',
|
||||
code: 'YOU_HAVE_BEEN_BLOCKED',
|
||||
id: 'b390d7e1-8a5e-46ed-b625-06271cafd3d3',
|
||||
},
|
||||
|
||||
accountLocked: {
|
||||
message: "You migrated. Your account is now locked.",
|
||||
code: "ACCOUNT_LOCKED",
|
||||
id: "d390d7e1-8a5e-46ed-b625-06271cafd3d3",
|
||||
message: 'You migrated. Your account is now locked.',
|
||||
code: 'ACCOUNT_LOCKED',
|
||||
id: 'd390d7e1-8a5e-46ed-b625-06271cafd3d3',
|
||||
},
|
||||
|
||||
needsEditId: {
|
||||
message: "You need to specify `editId`.",
|
||||
code: "NEEDS_EDIT_ID",
|
||||
id: "d697edc8-8c73-4de8-bded-35fd198b79e5",
|
||||
message: 'You need to specify `editId`.',
|
||||
code: 'NEEDS_EDIT_ID',
|
||||
id: 'd697edc8-8c73-4de8-bded-35fd198b79e5',
|
||||
},
|
||||
|
||||
noSuchNote: {
|
||||
message: "No such note.",
|
||||
code: "NO_SUCH_NOTE",
|
||||
id: "eef6c173-3010-4a23-8674-7c4fcaeba719",
|
||||
message: 'No such note.',
|
||||
code: 'NO_SUCH_NOTE',
|
||||
id: 'eef6c173-3010-4a23-8674-7c4fcaeba719',
|
||||
},
|
||||
|
||||
youAreNotTheAuthor: {
|
||||
message: "You are not the author of this note.",
|
||||
code: "YOU_ARE_NOT_THE_AUTHOR",
|
||||
id: "c6e61685-411d-43d0-b90a-a448d2539001",
|
||||
message: 'You are not the author of this note.',
|
||||
code: 'YOU_ARE_NOT_THE_AUTHOR',
|
||||
id: 'c6e61685-411d-43d0-b90a-a448d2539001',
|
||||
},
|
||||
|
||||
cannotPrivateRenote: {
|
||||
message: "You can not perform a private renote.",
|
||||
code: "CANNOT_PRIVATE_RENOTE",
|
||||
id: "19a50f1c-84fa-4e33-81d3-17834ccc0ad8",
|
||||
message: 'You can not perform a private renote.',
|
||||
code: 'CANNOT_PRIVATE_RENOTE',
|
||||
id: '19a50f1c-84fa-4e33-81d3-17834ccc0ad8',
|
||||
},
|
||||
|
||||
notLocalUser: {
|
||||
message: "You are not a local user.",
|
||||
code: "NOT_LOCAL_USER",
|
||||
id: "b907f407-2aa0-4283-800b-a2c56290b822",
|
||||
message: 'You are not a local user.',
|
||||
code: 'NOT_LOCAL_USER',
|
||||
id: 'b907f407-2aa0-4283-800b-a2c56290b822',
|
||||
},
|
||||
|
||||
cannotRenoteOutsideOfChannel: {
|
||||
@ -127,60 +127,63 @@ export const meta = {
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: "object",
|
||||
type: 'object',
|
||||
properties: {
|
||||
editId: { type: "string", format: "misskey:id" },
|
||||
visibility: { type: "string", enum: ['public', 'home', 'followers', 'specified'], default: "public" },
|
||||
editId: { type: 'string', format: 'misskey:id' },
|
||||
visibility: { type: 'string', enum: ['public', 'home', 'followers', 'specified'], default: 'public' },
|
||||
visibleUserIds: {
|
||||
type: "array",
|
||||
type: 'array',
|
||||
uniqueItems: true,
|
||||
items: {
|
||||
type: "string",
|
||||
format: "misskey:id",
|
||||
type: 'string',
|
||||
format: 'misskey:id',
|
||||
},
|
||||
},
|
||||
text: { type: "string", maxLength: MAX_NOTE_TEXT_LENGTH, nullable: true },
|
||||
cw: { type: "string", nullable: true, minLength: 1, maxLength: 250 },
|
||||
localOnly: { type: "boolean", default: false },
|
||||
noExtractMentions: { type: "boolean", default: false },
|
||||
noExtractHashtags: { type: "boolean", default: false },
|
||||
noExtractEmojis: { type: "boolean", default: false },
|
||||
cw: { type: 'string', nullable: true, minLength: 1, maxLength: 250 },
|
||||
localOnly: { type: 'boolean', default: false },
|
||||
reactionAcceptance: { type: 'string', nullable: true, enum: [null, 'likeOnly', 'likeOnlyForRemote', 'nonSensitiveOnly', 'nonSensitiveOnlyForLocalLikeOnlyForRemote'], default: null },
|
||||
noExtractMentions: { type: 'boolean', default: false },
|
||||
noExtractHashtags: { type: 'boolean', default: false },
|
||||
noExtractEmojis: { type: 'boolean', default: false },
|
||||
replyId: { type: 'string', format: 'misskey:id', nullable: true },
|
||||
renoteId: { type: 'string', format: 'misskey:id', nullable: true },
|
||||
channelId: { type: 'string', format: 'misskey:id', nullable: true },
|
||||
text: {
|
||||
type: 'string',
|
||||
minLength: 1,
|
||||
maxLength: MAX_NOTE_TEXT_LENGTH,
|
||||
nullable: true,
|
||||
},
|
||||
fileIds: {
|
||||
type: "array",
|
||||
type: 'array',
|
||||
uniqueItems: true,
|
||||
minItems: 1,
|
||||
maxItems: 16,
|
||||
items: { type: "string", format: "misskey:id" },
|
||||
items: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
mediaIds: {
|
||||
deprecated: true,
|
||||
description:
|
||||
"Use `fileIds` instead. If both are specified, this property is discarded.",
|
||||
type: "array",
|
||||
type: 'array',
|
||||
uniqueItems: true,
|
||||
minItems: 1,
|
||||
maxItems: 16,
|
||||
items: { type: "string", format: "misskey:id" },
|
||||
items: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
replyId: { type: "string", format: "misskey:id", nullable: true },
|
||||
renoteId: { type: "string", format: "misskey:id", nullable: true },
|
||||
channelId: { type: "string", format: "misskey:id", nullable: true },
|
||||
poll: {
|
||||
type: "object",
|
||||
type: 'object',
|
||||
nullable: true,
|
||||
properties: {
|
||||
choices: {
|
||||
type: "array",
|
||||
type: 'array',
|
||||
uniqueItems: true,
|
||||
minItems: 2,
|
||||
maxItems: 10,
|
||||
items: { type: "string", minLength: 1, maxLength: 50 },
|
||||
items: { type: 'string', minLength: 1, maxLength: 50 },
|
||||
},
|
||||
multiple: { type: "boolean", default: false },
|
||||
expiresAt: { type: "integer", nullable: true },
|
||||
expiredAfter: { type: "integer", nullable: true, minimum: 1 },
|
||||
multiple: { type: 'boolean' },
|
||||
expiresAt: { type: 'integer', nullable: true },
|
||||
expiredAfter: { type: 'integer', nullable: true, minimum: 1 },
|
||||
},
|
||||
required: ["choices"],
|
||||
required: ['choices'],
|
||||
},
|
||||
},
|
||||
anyOf: [
|
||||
@ -188,32 +191,32 @@ export const paramDef = {
|
||||
// (re)note with text, files and poll are optional
|
||||
properties: {
|
||||
text: {
|
||||
type: "string",
|
||||
type: 'string',
|
||||
minLength: 1,
|
||||
maxLength: MAX_NOTE_TEXT_LENGTH,
|
||||
nullable: false,
|
||||
},
|
||||
},
|
||||
required: ["text"],
|
||||
required: ['text'],
|
||||
},
|
||||
{
|
||||
// (re)note with files, text and poll are optional
|
||||
required: ["fileIds"],
|
||||
required: ['fileIds'],
|
||||
},
|
||||
{
|
||||
// (re)note with files, text and poll are optional
|
||||
required: ["mediaIds"],
|
||||
required: ['mediaIds'],
|
||||
},
|
||||
{
|
||||
// (re)note with poll, text and files are optional
|
||||
properties: {
|
||||
poll: { type: "object", nullable: false },
|
||||
poll: { type: 'object', nullable: false },
|
||||
},
|
||||
required: ["poll"],
|
||||
required: ['poll'],
|
||||
},
|
||||
{
|
||||
// pure renote
|
||||
required: ["renoteId"],
|
||||
required: ['renoteId'],
|
||||
},
|
||||
],
|
||||
} as const;
|
||||
|
@ -139,7 +139,9 @@ export class ClientServerService {
|
||||
'type': 'image/png',
|
||||
'purpose': 'maskable',
|
||||
}, {
|
||||
'src': '/static-assets/splash.png',
|
||||
// 空文字列の場合右辺を使いたいため
|
||||
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
||||
'src': instance.app512IconUrl || '/static-assets/icons/512.png',
|
||||
'sizes': '300x300',
|
||||
'type': 'image/png',
|
||||
'purpose': 'any',
|
||||
|
@ -39,9 +39,7 @@ export async function mainBoot() {
|
||||
|
||||
let reloadDialogShowing = false;
|
||||
stream.on('_disconnected_', async () => {
|
||||
if (defaultStore.state.serverDisconnectedBehavior === 'reload') {
|
||||
location.reload();
|
||||
} else if (defaultStore.state.serverDisconnectedBehavior === 'dialog') {
|
||||
if (defaultStore.state.serverDisconnectedBehavior === 'dialog') {
|
||||
if (reloadDialogShowing) return;
|
||||
reloadDialogShowing = true;
|
||||
const { canceled } = await confirm({
|
||||
|
@ -57,6 +57,48 @@ const avatarUrl = computed(() => defaultStore.state.disableShowingAnimatedImages
|
||||
}
|
||||
}
|
||||
|
||||
.root + .root {
|
||||
position: relative;
|
||||
margin-inline: -20px 0;
|
||||
box-shadow: -4px 0 0 var(--panel), -15px 0 15px var(--panel);
|
||||
overflow: clip;
|
||||
isolation: isolate;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: var(--panel);
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: var(--panel);
|
||||
z-index: -1;
|
||||
background: inherit;
|
||||
}
|
||||
|
||||
span {
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
max-width: 3em;
|
||||
mask: linear-gradient(to right, #000 20%, rgba(0, 0, 0, 0.4));
|
||||
}
|
||||
|
||||
+ .root {
|
||||
margin-inline: -10px 0;
|
||||
padding-inline-end: 0;
|
||||
box-shadow: -4px 0 0 var(--panel);
|
||||
|
||||
span {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 1.5em;
|
||||
height: 1.5em;
|
||||
|
@ -127,9 +127,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
ref="quoteButton"
|
||||
:class="$style.footerButton"
|
||||
class="_button"
|
||||
:style="quoted ? 'color: var(--accent) !important;' : ''"
|
||||
v-on:click.stop
|
||||
@mousedown="quoted ? undoQuote(appearNote) : quote()"
|
||||
@mousedown="quote()"
|
||||
>
|
||||
<i class="ph-quotes ph-bold ph-lg"></i>
|
||||
</button>
|
||||
@ -226,7 +225,10 @@ const currentClip = inject<Ref<Misskey.entities.Clip> | null>('currentClip', nul
|
||||
let note = $ref(deepClone(props.note));
|
||||
|
||||
function noteclick(id: string) {
|
||||
router.push(`/notes/${id}`);
|
||||
const selection = document.getSelection();
|
||||
if (selection?.toString().length === 0) {
|
||||
router.push(`/notes/${id}`);
|
||||
}
|
||||
}
|
||||
|
||||
// plugin
|
||||
@ -278,14 +280,13 @@ const isLong = shouldCollapsed(appearNote, urls ?? []);
|
||||
const collapsed = ref(appearNote.cw == null && isLong);
|
||||
const isDeleted = ref(false);
|
||||
const renoted = ref(false);
|
||||
const quoted = ref(false);
|
||||
const muted = ref($i ? checkWordMute(appearNote, $i, $i.mutedWords) : false);
|
||||
const translation = ref<any>(null);
|
||||
const translating = ref(false);
|
||||
const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.user.instance);
|
||||
const canRenote = computed(() => ['public', 'home'].includes(appearNote.visibility) || (appearNote.visibility === 'followers' && appearNote.userId === $i.id));
|
||||
let renoteCollapsed = $ref(defaultStore.state.collapseRenotes && isRenote && (($i && ($i.id === note.userId || $i.id === appearNote.userId)) || (appearNote.myReaction != null)));
|
||||
const defaultLike = computed(() => defaultStore.state.like !== '❤️' ? defaultStore.state.like : null);
|
||||
const defaultLike = computed(() => defaultStore.state.like ? defaultStore.state.like : null);
|
||||
|
||||
const keymap = {
|
||||
'r': () => reply(true),
|
||||
@ -364,15 +365,6 @@ if (!props.mock) {
|
||||
}).then((res) => {
|
||||
renoted.value = res.length > 0;
|
||||
});
|
||||
|
||||
os.api("notes/renotes", {
|
||||
noteId: appearNote.id,
|
||||
userId: $i.id,
|
||||
limit: 1,
|
||||
quote: true,
|
||||
}).then((res) => {
|
||||
quoted.value = res.length > 0;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -467,7 +459,6 @@ function quote() {
|
||||
os.popup(MkRippleEffect, { x, y }, {}, 'end');
|
||||
}
|
||||
|
||||
quoted.value = res.length > 0;
|
||||
os.toast(i18n.ts.quoted);
|
||||
});
|
||||
});
|
||||
@ -490,7 +481,6 @@ function quote() {
|
||||
os.popup(MkRippleEffect, { x, y }, {}, 'end');
|
||||
}
|
||||
|
||||
quoted.value = res.length > 0;
|
||||
os.toast(i18n.ts.quoted);
|
||||
});
|
||||
});
|
||||
@ -603,26 +593,6 @@ function undoRenote(note) : void {
|
||||
}
|
||||
}
|
||||
|
||||
function undoQuote(note) : void {
|
||||
if (props.mock) {
|
||||
return;
|
||||
}
|
||||
os.api("notes/unrenote", {
|
||||
noteId: note.id,
|
||||
quote: true
|
||||
});
|
||||
os.toast(i18n.ts.rmquote);
|
||||
quoted.value = false;
|
||||
|
||||
const el = quoteButton.value as HTMLElement | null | undefined;
|
||||
if (el) {
|
||||
const rect = el.getBoundingClientRect();
|
||||
const x = rect.left + (el.offsetWidth / 2);
|
||||
const y = rect.top + (el.offsetHeight / 2);
|
||||
os.popup(MkRippleEffect, { x, y }, {}, 'end');
|
||||
}
|
||||
}
|
||||
|
||||
function onContextmenu(ev: MouseEvent): void {
|
||||
if (props.mock) {
|
||||
return;
|
||||
|
@ -137,8 +137,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
ref="quoteButton"
|
||||
class="_button"
|
||||
:class="$style.noteFooterButton"
|
||||
:style="quoted ? 'color: var(--accent) !important;' : ''"
|
||||
@mousedown="quoted ? undoQuote() : quote()"
|
||||
@mousedown="quote()"
|
||||
>
|
||||
<i class="ph-quotes ph-bold ph-lg"></i>
|
||||
</button>
|
||||
@ -310,7 +309,6 @@ const isMyRenote = $i && ($i.id === note.userId);
|
||||
const showContent = ref(false);
|
||||
const isDeleted = ref(false);
|
||||
const renoted = ref(false);
|
||||
const quoted = ref(false);
|
||||
const muted = ref($i ? checkWordMute(appearNote, $i, $i.mutedWords) : false);
|
||||
const translation = ref(null);
|
||||
const translating = ref(false);
|
||||
@ -323,7 +321,7 @@ const conversation = ref<Misskey.entities.Note[]>([]);
|
||||
const replies = ref<Misskey.entities.Note[]>([]);
|
||||
const quotes = ref<Misskey.entities.Note[]>([]);
|
||||
const canRenote = computed(() => ['public', 'home'].includes(appearNote.visibility) || appearNote.userId === $i.id);
|
||||
const defaultLike = computed(() => defaultStore.state.like !== '❤️' ? defaultStore.state.like : null);
|
||||
const defaultLike = computed(() => defaultStore.state.like ? defaultStore.state.like : null);
|
||||
|
||||
watch(() => props.expandAllCws, (expandAllCws) => {
|
||||
if (expandAllCws !== showContent.value) showContent.value = expandAllCws;
|
||||
@ -337,15 +335,6 @@ if ($i) {
|
||||
}).then((res) => {
|
||||
renoted.value = res.length > 0;
|
||||
});
|
||||
|
||||
os.api("notes/renotes", {
|
||||
noteId: appearNote.id,
|
||||
userId: $i.id,
|
||||
limit: 1,
|
||||
quote: true,
|
||||
}).then((res) => {
|
||||
quoted.value = res.length > 0;
|
||||
});
|
||||
}
|
||||
|
||||
const keymap = {
|
||||
@ -511,7 +500,6 @@ function quote() {
|
||||
os.popup(MkRippleEffect, { x, y }, {}, 'end');
|
||||
}
|
||||
|
||||
quoted.value = res.length > 0;
|
||||
os.toast(i18n.ts.quoted);
|
||||
});
|
||||
});
|
||||
@ -534,7 +522,6 @@ function quote() {
|
||||
os.popup(MkRippleEffect, { x, y }, {}, 'end');
|
||||
}
|
||||
|
||||
quoted.value = res.length > 0;
|
||||
os.toast(i18n.ts.quoted);
|
||||
});
|
||||
});
|
||||
@ -625,23 +612,6 @@ function undoRenote() : void {
|
||||
}
|
||||
}
|
||||
|
||||
function undoQuote() : void {
|
||||
os.api("notes/unrenote", {
|
||||
noteId: appearNote.id,
|
||||
quote: true
|
||||
});
|
||||
os.toast(i18n.ts.rmquote);
|
||||
quoted.value = false;
|
||||
|
||||
const el = quoteButton.value as HTMLElement | null | undefined;
|
||||
if (el) {
|
||||
const rect = el.getBoundingClientRect();
|
||||
const x = rect.left + (el.offsetWidth / 2);
|
||||
const y = rect.top + (el.offsetHeight / 2);
|
||||
os.popup(MkRippleEffect, { x, y }, {}, 'end');
|
||||
}
|
||||
}
|
||||
|
||||
function onContextmenu(ev: MouseEvent): void {
|
||||
const isLink = (el: HTMLElement) => {
|
||||
if (el.tagName === 'A') return true;
|
||||
|
@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<div>
|
||||
<p v-if="note.cw != null" :class="$style.cw">
|
||||
<Mfm v-if="note.cw != ''" style="margin-right: 8px;" :text="note.cw" :author="note.user" :nyaize="'respect'" :emojiUrls="note.emojis"/>
|
||||
<MkCwButton v-model="showContent" :note="note"/>
|
||||
<MkCwButton v-model="showContent" :note="note" v-on:click.stop/>
|
||||
</p>
|
||||
<div v-show="note.cw == null || showContent">
|
||||
<MkSubNoteContent :hideFiles="hideFiles" :class="$style.text" :note="note"/>
|
||||
|
@ -41,8 +41,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
ref="quoteButton"
|
||||
class="_button"
|
||||
:class="$style.noteFooterButton"
|
||||
:style="quoted ? 'color: var(--accent) !important;' : ''"
|
||||
@mousedown="quoted ? undoQuote() : quote()"
|
||||
@mousedown="quote()"
|
||||
>
|
||||
<i class="ph-quotes ph-bold ph-lg"></i>
|
||||
</button>
|
||||
@ -125,7 +124,6 @@ const translation = ref<any>(null);
|
||||
const translating = ref(false);
|
||||
const isDeleted = ref(false);
|
||||
const renoted = ref(false);
|
||||
const quoted = ref(false);
|
||||
const reactButton = shallowRef<HTMLElement>();
|
||||
const renoteButton = shallowRef<HTMLElement>();
|
||||
const quoteButton = shallowRef<HTMLElement>();
|
||||
@ -133,7 +131,7 @@ const menuButton = shallowRef<HTMLElement>();
|
||||
const likeButton = shallowRef<HTMLElement>();
|
||||
|
||||
let appearNote = $computed(() => isRenote ? props.note.renote as Misskey.entities.Note : props.note);
|
||||
const defaultLike = computed(() => defaultStore.state.like !== '❤️' ? defaultStore.state.like : null);
|
||||
const defaultLike = computed(() => defaultStore.state.like ? defaultStore.state.like : null);
|
||||
|
||||
const isRenote = (
|
||||
props.note.renote != null &&
|
||||
@ -156,15 +154,6 @@ if ($i) {
|
||||
}).then((res) => {
|
||||
renoted.value = res.length > 0;
|
||||
});
|
||||
|
||||
os.api("notes/renotes", {
|
||||
noteId: appearNote.id,
|
||||
userId: $i.id,
|
||||
limit: 1,
|
||||
quote: true,
|
||||
}).then((res) => {
|
||||
quoted.value = res.length > 0;
|
||||
});
|
||||
}
|
||||
|
||||
function focus() {
|
||||
@ -255,23 +244,6 @@ function undoRenote() : void {
|
||||
}
|
||||
}
|
||||
|
||||
function undoQuote() : void {
|
||||
os.api("notes/unrenote", {
|
||||
noteId: appearNote.id,
|
||||
quote: true
|
||||
});
|
||||
os.toast(i18n.ts.rmquote);
|
||||
quoted.value = false;
|
||||
|
||||
const el = quoteButton.value as HTMLElement | null | undefined;
|
||||
if (el) {
|
||||
const rect = el.getBoundingClientRect();
|
||||
const x = rect.left + (el.offsetWidth / 2);
|
||||
const y = rect.top + (el.offsetHeight / 2);
|
||||
os.popup(MkRippleEffect, { x, y }, {}, 'end');
|
||||
}
|
||||
}
|
||||
|
||||
let showContent = $ref(false);
|
||||
|
||||
watch(() => props.expandAllCws, (expandAllCws) => {
|
||||
@ -342,7 +314,6 @@ function quote() {
|
||||
os.popup(MkRippleEffect, { x, y }, {}, 'end');
|
||||
}
|
||||
|
||||
quoted.value = res.length > 0;
|
||||
os.toast(i18n.ts.quoted);
|
||||
});
|
||||
});
|
||||
@ -365,7 +336,6 @@ function quote() {
|
||||
os.popup(MkRippleEffect, { x, y }, {}, 'end');
|
||||
}
|
||||
|
||||
quoted.value = res.length > 0;
|
||||
os.toast(i18n.ts.quoted);
|
||||
});
|
||||
});
|
||||
|
@ -17,6 +17,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</ul>
|
||||
<p v-if="!readOnly" :class="$style.info">
|
||||
<span>{{ i18n.t('_poll.totalVotes', { n: total }) }}</span>
|
||||
<span v-if="note.poll.multiple"> · </span>
|
||||
<span v-if="note.poll.multiple">{{ i18n.ts._poll.multiple }}</span>
|
||||
<span> · </span>
|
||||
<a v-if="!closed && !isVoted" style="color: inherit;" @click="showResult = !showResult">{{ showResult ? i18n.ts._poll.vote : i18n.ts._poll.showResult }}</a>
|
||||
<span v-if="isVoted">{{ i18n.ts._poll.voted }}</span>
|
||||
@ -78,12 +80,19 @@ const vote = async (id) => {
|
||||
pleaseLogin();
|
||||
|
||||
if (props.readOnly || closed.value || isVoted.value) return;
|
||||
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'question',
|
||||
text: i18n.t('voteConfirm', { choice: props.note.poll.choices[id].text }),
|
||||
});
|
||||
if (canceled) return;
|
||||
if (!props.note.poll.multiple) {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'question',
|
||||
text: i18n.t('voteConfirm', { choice: props.note.poll.choices[id].text }),
|
||||
});
|
||||
if (canceled) return;
|
||||
} else {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'question',
|
||||
text: i18n.t('voteConfirmMulti', { choice: props.note.poll.choices[id].text }),
|
||||
});
|
||||
if (canceled) return;
|
||||
}
|
||||
|
||||
await os.api('notes/polls/vote', {
|
||||
noteId: props.note.id,
|
||||
|
@ -931,8 +931,8 @@ onMounted(() => {
|
||||
poll = {
|
||||
choices: init.poll.choices.map(x => x.text),
|
||||
multiple: init.poll.multiple,
|
||||
expiresAt: init.poll.expiresAt,
|
||||
expiredAfter: init.poll.expiredAfter,
|
||||
expiresAt: init.poll.expiresAt ? new Date(init.poll.expiresAt).getTime().toString() : null,
|
||||
expiredAfter: init.poll.expiredAfter ? new Date(init.poll.expiredAfter).getTime().toString() : null,
|
||||
};
|
||||
}
|
||||
visibility = init.visibility;
|
||||
|
@ -16,7 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<MkLoading v-if="translating" mini/>
|
||||
<div v-else>
|
||||
<b>{{ i18n.t('translatedFrom', { x: translation.sourceLang }) }}: </b>
|
||||
<Mfm :text="translation.text" :author="note.user" :nyaize="'account'" :emojiUrls="note.emojis"/>
|
||||
<Mfm :text="translation.text" :author="note.user" :nyaize="'respect'" :emojiUrls="note.emojis"/>
|
||||
</div>
|
||||
</div>
|
||||
<MkA v-if="note.renoteId" :class="$style.rp" :to="`/notes/${note.renoteId}`" v-on:click.stop>RN: ...</MkA>
|
||||
@ -63,7 +63,10 @@ const props = defineProps<{
|
||||
const router = useRouter();
|
||||
|
||||
function noteclick(id: string) {
|
||||
router.push(`/notes/${id}`);
|
||||
const selection = document.getSelection();
|
||||
if (selection?.toString().length === 0) {
|
||||
router.push(`/notes/${id}`);
|
||||
}
|
||||
}
|
||||
|
||||
const parsed = $computed(() => props.note.text ? mfm.parse(props.note.text) : null);
|
||||
|
@ -28,9 +28,20 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<div :class="$style.username"><MkAcct :user="user"/></div>
|
||||
</div>
|
||||
<div :class="$style.description">
|
||||
<Mfm v-if="user.description" :class="$style.mfm" :text="user.description" :author="user"/>
|
||||
<Mfm v-if="user.description" :nyaize="false" :class="$style.mfm" :text="user.description" :author="user"/>
|
||||
<div v-else style="opacity: 0.7;">{{ i18n.ts.noAccountDescription }}</div>
|
||||
</div>
|
||||
<div v-if="user.fields.length > 0" :class="$style.fields">
|
||||
<dl v-for="(field, i) in user.fields" :key="i" :class="$style.field">
|
||||
<dt :class="$style.fieldname">
|
||||
<Mfm :text="field.name" :nyaize="false" :plain="true" :colored="false"/>
|
||||
</dt>
|
||||
<dd :class="$style.fieldvalue">
|
||||
<Mfm :text="field.value" :nyaize="false" :author="user" :colored="false"/>
|
||||
<i v-if="user.verifiedLinks.includes(field.value)" v-tooltip:dialog="i18n.ts.verifiedLink" class="ph-seal-check ph-bold ph-lg" :class="$style.verifiedLink"></i>
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
<div :class="$style.status">
|
||||
<div :class="$style.statusItem">
|
||||
<div :class="$style.statusItemLabel">{{ i18n.ts.notes }}</div>
|
||||
@ -221,6 +232,48 @@ onMounted(() => {
|
||||
border-bottom: solid 1px var(--divider);
|
||||
}
|
||||
|
||||
.fields {
|
||||
font-size: 0.8em;
|
||||
padding: 16px;
|
||||
border-top: solid 1px var(--divider);
|
||||
border-bottom: solid 1px var(--divider);
|
||||
}
|
||||
|
||||
.field {
|
||||
display: flex;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
:deep(span) {
|
||||
white-space: nowrap !important;
|
||||
}
|
||||
}
|
||||
|
||||
.fieldvalue {
|
||||
width: 70%;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
word-wrap: nowrap;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.fieldname {
|
||||
width: 100px;
|
||||
max-height: 45px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
display: inline;
|
||||
text-overflow: ellipsis;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
padding-inline-end: 10px;
|
||||
}
|
||||
|
||||
.mfm {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 5;
|
||||
|
@ -65,6 +65,13 @@ const props = defineProps<{
|
||||
edit: boolean;
|
||||
}>();
|
||||
|
||||
// This will not be available for now as I don't think this is needed
|
||||
// const notesSearchAvailable = (($i == null && instance.policies.canSearchNotes) || ($i != null && $i.policies.canSearchNotes));
|
||||
/* if (!notesSearchAvailable) {
|
||||
const wid = widgetDefs.findIndex(widget => widget === 'search');
|
||||
widgetDefs.splice(wid, 1);
|
||||
} */
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'updateWidgets', widgets: Widget[]): void;
|
||||
(ev: 'addWidget', widget: Widget): void;
|
||||
|
30
packages/frontend/src/components/SkSearchResultWindow.vue
Normal file
30
packages/frontend/src/components/SkSearchResultWindow.vue
Normal file
@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<MkWindow ref="window" :initialWidth="600" :initialHeight="450" :canResize="true" @closed="emit('closed')">
|
||||
<template #header>
|
||||
<i class="ph-magnifying-glass ph-bold ph-lg" style="margin-right: 0.5em;"></i>
|
||||
<b>Result</b>
|
||||
</template>
|
||||
<MkNotes :key="props.noteKey" :pagination="props.notePagination"/>
|
||||
</MkWindow>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import type { Paging } from '@/components/MkPagination.vue';
|
||||
import MkNotes from '@/components/MkNotes.vue';
|
||||
import MkWindow from '@/components/MkWindow.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
noteKey: string | number | symbol | undefined;
|
||||
notePagination: Paging;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'closed'): void;
|
||||
}>();
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
||||
</style>
|
@ -82,7 +82,13 @@ export default function(props: MfmProps) {
|
||||
res.push(t);
|
||||
}
|
||||
res.shift();
|
||||
return res;
|
||||
|
||||
// Don't wrap whitespaces in a span
|
||||
if (text === ' ') {
|
||||
return res;
|
||||
}
|
||||
|
||||
return h('span', res);
|
||||
} else {
|
||||
return [text.replace(/\n/g, ' ')];
|
||||
}
|
||||
|
@ -216,6 +216,7 @@ onUnmounted(() => {
|
||||
|
||||
&.active {
|
||||
opacity: 1;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
&.animate {
|
||||
|
@ -48,7 +48,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</MkInput>
|
||||
|
||||
<FromSlot>
|
||||
<template #label>Default like emoji</template>
|
||||
<template #label>{{ i18n.ts.defaultLike }}</template>
|
||||
<MkCustomEmoji v-if="defaultLike.startsWith(':')" style="max-height: 3em; font-size: 1.1em;" :useOriginalSize="false" :class="$style.reaction" :name="defaultLike" :normal="true" :noStyle="true"/>
|
||||
<MkEmoji v-else :emoji="defaultLike" style="max-height: 3em; font-size: 1.1em;" :normal="true" :noStyle="true"/>
|
||||
<MkButton rounded :small="true" @click="chooseNewLike"><i class="ph-smiley ph-bold ph-lg"></i> Change</MkButton>
|
||||
|
@ -166,7 +166,6 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</div>
|
||||
<MkSelect v-model="serverDisconnectedBehavior">
|
||||
<template #label>{{ i18n.ts.whenServerDisconnected }}</template>
|
||||
<option value="reload">{{ i18n.ts._serverDisconnectedBehavior.reload }}</option>
|
||||
<option value="dialog">{{ i18n.ts._serverDisconnectedBehavior.dialog }}</option>
|
||||
<option value="quiet">{{ i18n.ts._serverDisconnectedBehavior.quiet }}</option>
|
||||
<option value="disabled">{{ i18n.ts._serverDisconnectedBehavior.disabled }}</option>
|
||||
|
@ -24,9 +24,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</FromSlot>
|
||||
|
||||
<FromSlot>
|
||||
<template #label>Default like emoji</template>
|
||||
<MkCustomEmoji v-if="like.startsWith(':')" style="max-height: 3em; font-size: 1.1em;" :useOriginalSize="false" :class="$style.reaction" :name="like" :normal="true" :noStyle="true"/>
|
||||
<MkEmoji v-else :emoji="like" style="max-height: 3em; font-size: 1.1em;" :normal="true" :noStyle="true"/>
|
||||
<template #label>{{ i18n.ts.defaultLike }}</template>
|
||||
<MkCustomEmoji v-if="like && like.startsWith(':')" style="max-height: 3em; font-size: 1.1em;" :useOriginalSize="false" :class="$style.reaction" :name="like" :normal="true" :noStyle="true"/>
|
||||
<MkEmoji v-else-if="like && !like.startsWith(':')" :emoji="like" style="max-height: 3em; font-size: 1.1em;" :normal="true" :noStyle="true"/>
|
||||
<span v-else-if="!like">{{ i18n.ts.notSet }}</span>
|
||||
<div class="_buttons" style="padding-top: 8px;">
|
||||
<MkButton rounded :small="true" inline @click="chooseNewLike"><i class="ph-smiley ph-bold ph-lg"></i> Change</MkButton>
|
||||
<MkButton rounded :small="true" inline @click="resetLike"><i class="ph-arrow-clockwise ph-bold ph-lg"></i> Reset</MkButton>
|
||||
@ -82,6 +83,7 @@ import { defaultStore } from '@/store.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { deepClone } from '@/scripts/clone.js';
|
||||
import { unisonReload } from '@/scripts/unison-reload.js';
|
||||
|
||||
let reactions = $ref(deepClone(defaultStore.state.reactions));
|
||||
const like = $computed(defaultStore.makeGetterSetter('like'));
|
||||
@ -91,6 +93,16 @@ const reactionPickerWidth = $computed(defaultStore.makeGetterSetter('reactionPic
|
||||
const reactionPickerHeight = $computed(defaultStore.makeGetterSetter('reactionPickerHeight'));
|
||||
const reactionPickerUseDrawerForMobile = $computed(defaultStore.makeGetterSetter('reactionPickerUseDrawerForMobile'));
|
||||
|
||||
async function reloadAsk() {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'info',
|
||||
text: i18n.ts.reloadToApplySetting,
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
unisonReload();
|
||||
}
|
||||
|
||||
function save() {
|
||||
defaultStore.set('reactions', reactions);
|
||||
}
|
||||
@ -134,13 +146,15 @@ function chooseEmoji(ev: MouseEvent) {
|
||||
function chooseNewLike(ev: MouseEvent) {
|
||||
os.pickEmoji(ev.currentTarget ?? ev.target, {
|
||||
showPinned: false,
|
||||
}).then(emoji => {
|
||||
}).then(async emoji => {
|
||||
defaultStore.set('like', emoji as string);
|
||||
await reloadAsk();
|
||||
});
|
||||
}
|
||||
|
||||
function resetLike() {
|
||||
defaultStore.set('like', '❤️');
|
||||
async function resetLike() {
|
||||
defaultStore.set('like', null);
|
||||
await reloadAsk();
|
||||
}
|
||||
|
||||
watch($$(reactions), () => {
|
||||
|
@ -176,7 +176,13 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: Router
|
||||
const canonical = user.host === null ? `@${user.username}` : `@${user.username}@${toUnicode(user.host)}`;
|
||||
copyToClipboard(`${url}/${canonical}`);
|
||||
},
|
||||
}, {
|
||||
}, ...(user.host ? [{
|
||||
icon: 'ph-share ph-bold ph-lg',
|
||||
text: i18n.ts.openRemoteProfile,
|
||||
action: () => {
|
||||
open(`${user.uri}`, '_blank');
|
||||
},
|
||||
}] : []), {
|
||||
icon: 'ph-envelope ph-bold ph-lg',
|
||||
text: i18n.ts.sendMessage,
|
||||
action: () => {
|
||||
|
@ -112,7 +112,7 @@ export const defaultStore = markRaw(new Storage('base', {
|
||||
},
|
||||
like: {
|
||||
where: 'account',
|
||||
default: '❤️',
|
||||
default: null as string | null,
|
||||
},
|
||||
mutedAds: {
|
||||
where: 'account',
|
||||
@ -188,7 +188,7 @@ export const defaultStore = markRaw(new Storage('base', {
|
||||
},
|
||||
serverDisconnectedBehavior: {
|
||||
where: 'device',
|
||||
default: 'quiet' as 'quiet' | 'reload' | 'dialog' | 'disabled',
|
||||
default: 'disabled' as 'quiet' | 'dialog' | 'disabled',
|
||||
},
|
||||
nsfw: {
|
||||
where: 'device',
|
||||
|
@ -109,7 +109,8 @@ watch(defaultStore.reactiveState.menuDisplay, () => {
|
||||
$nav-icon-only-width: 78px; // TODO: どこかに集約したい
|
||||
$avatar-size: 32px;
|
||||
$avatar-margin: 8px;
|
||||
|
||||
position: sticky;
|
||||
top: 16px;
|
||||
padding: 0 16px;
|
||||
box-sizing: border-box;
|
||||
width: 260px;
|
||||
|
@ -253,9 +253,13 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
> .widgets {
|
||||
//--panelBorder: none;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
width: 300px;
|
||||
padding-bottom: calc(var(--margin) + env(safe-area-inset-bottom, 0px));
|
||||
height: 100%;
|
||||
padding-top: 16px;
|
||||
box-sizing: border-box;
|
||||
overflow: auto;
|
||||
|
||||
@media (max-width: $widgets-hide-threshold) {
|
||||
display: none;
|
||||
|
@ -324,7 +324,7 @@ $widgets-hide-threshold: 1090px;
|
||||
min-width: 0;
|
||||
overflow: auto;
|
||||
overflow-y: scroll;
|
||||
overscroll-behavior: contain;
|
||||
overscroll-behavior: unset;
|
||||
background: var(--bg);
|
||||
}
|
||||
|
||||
|
155
packages/frontend/src/widgets/WidgetSearch.vue
Normal file
155
packages/frontend/src/widgets/WidgetSearch.vue
Normal file
@ -0,0 +1,155 @@
|
||||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<MkContainer :showHeader="widgetProps.showHeader" class="skw-search">
|
||||
<MkInput v-model="searchQuery" :large="true" :autofocus="true" type="search" @keydown="onInputKeydown">
|
||||
<template #suffix>
|
||||
<button style="border: none; background: none; margin-right: 0.5em; z-index: 2; pointer-events: auto; position: relative; margin-top: 0 auto;" @click="options"><i class="ph-funnel ph-bold ph-lg"></i></button>
|
||||
<button style="border: none; background: none; z-index: 2; pointer-events: auto; position: relative; margin: 0 auto;" @click="search"><i class="ph-magnifying-glass ph-bold ph-lg"></i></button>
|
||||
</template>
|
||||
</MkInput>
|
||||
</MkContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, defineAsyncComponent, onMounted } from 'vue';
|
||||
import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import MkContainer from '@/components/MkContainer.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import * as os from '@/os.js';
|
||||
import { useRouter } from '@/router.js';
|
||||
import { GetFormResultType } from '@/scripts/form.js';
|
||||
|
||||
const name = 'search';
|
||||
|
||||
const widgetPropsDef = {
|
||||
showHeader: {
|
||||
type: 'boolean' as const,
|
||||
default: false,
|
||||
},
|
||||
};
|
||||
|
||||
type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
|
||||
|
||||
const props = defineProps<WidgetComponentProps<WidgetProps>>();
|
||||
const emit = defineEmits<WidgetComponentEmits<WidgetProps>>();
|
||||
|
||||
const { widgetProps, configure } = useWidgetPropsManager(name,
|
||||
widgetPropsDef,
|
||||
props,
|
||||
emit,
|
||||
);
|
||||
|
||||
function onInputKeydown(evt: KeyboardEvent) {
|
||||
if (evt.key === 'Enter') {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
search();
|
||||
}
|
||||
}
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
let key = $ref(0);
|
||||
let searchQuery = $ref('');
|
||||
let notePagination = $ref();
|
||||
let searchOrigin = $ref('combined');
|
||||
let user = $ref(null);
|
||||
let isLocalOnly = $ref(false);
|
||||
let order = $ref(true);
|
||||
let filetype = $ref(null);
|
||||
|
||||
function options(ev) {
|
||||
os.popupMenu([{
|
||||
type: 'parent',
|
||||
text: 'With File',
|
||||
icon: 'ph-file ph-bold ph-lg',
|
||||
children: [
|
||||
{
|
||||
type: 'button',
|
||||
icon: 'ph-image ph-bold ph-lg',
|
||||
text: 'With Images',
|
||||
action: () => {
|
||||
filetype = 'image';
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'button',
|
||||
icon: 'ph-music-notes-simple ph-bold ph-lg',
|
||||
text: 'With Audios',
|
||||
action: () => {
|
||||
filetype = 'audio';
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'button',
|
||||
icon: 'ph-video ph-bold ph-lg',
|
||||
text: 'With Videos',
|
||||
action: () => {
|
||||
filetype = 'video';
|
||||
},
|
||||
}],
|
||||
}], ev.currentTarget ?? ev.target);
|
||||
}
|
||||
|
||||
function selectUser() {
|
||||
os.selectUser().then(_user => {
|
||||
user = _user;
|
||||
});
|
||||
}
|
||||
|
||||
async function search() {
|
||||
const query = searchQuery.toString().trim();
|
||||
|
||||
if (query == null || query === '') return;
|
||||
|
||||
if (query.startsWith('https://')) {
|
||||
const promise = os.api('ap/show', {
|
||||
uri: query,
|
||||
});
|
||||
|
||||
os.promiseDialog(promise, null, null, i18n.ts.fetchingAsApObject);
|
||||
|
||||
const res = await promise;
|
||||
|
||||
if (res.type === 'User') {
|
||||
router.push(`/@${res.object.username}@${res.object.host}`);
|
||||
} else if (res.type === 'Note') {
|
||||
router.push(`/notes/${res.object.id}`);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
notePagination = {
|
||||
endpoint: 'notes/search',
|
||||
limit: 10,
|
||||
params: {
|
||||
query: searchQuery,
|
||||
userId: user ? user.id : null,
|
||||
order: order ? 'desc' : 'asc',
|
||||
filetype: filetype,
|
||||
},
|
||||
};
|
||||
|
||||
if (isLocalOnly) notePagination.params.host = '.';
|
||||
|
||||
key++;
|
||||
|
||||
os.popup(defineAsyncComponent(() => import('@/components/SkSearchResultWindow.vue')), {
|
||||
noteKey: key,
|
||||
notePagination: notePagination,
|
||||
}, {
|
||||
}, 'closed');
|
||||
}
|
||||
|
||||
defineExpose<WidgetComponentExpose>({
|
||||
name,
|
||||
configure,
|
||||
id: props.widget ? props.widget.id : null,
|
||||
});
|
||||
</script>
|
@ -33,6 +33,7 @@ export default function(app: App) {
|
||||
app.component('WidgetAichan', defineAsyncComponent(() => import('./WidgetAichan.vue')));
|
||||
app.component('WidgetUserList', defineAsyncComponent(() => import('./WidgetUserList.vue')));
|
||||
app.component('WidgetClicker', defineAsyncComponent(() => import('./WidgetClicker.vue')));
|
||||
app.component('WidgetSearch', defineAsyncComponent(() => import('./WidgetSearch.vue')));
|
||||
}
|
||||
|
||||
export const widgets = [
|
||||
@ -63,4 +64,5 @@ export const widgets = [
|
||||
'aichan',
|
||||
'userList',
|
||||
'clicker',
|
||||
'search',
|
||||
];
|
||||
|
@ -63,7 +63,7 @@
|
||||
"@types/parse-link-header": "^2.0.3",
|
||||
"@types/uuid": "^9.0.7",
|
||||
"@types/ws": "^8.5.10",
|
||||
"axios": "1.5.0",
|
||||
"axios": "1.6.0",
|
||||
"dayjs": "^1.11.10",
|
||||
"form-data": "^4.0.0",
|
||||
"https-proxy-agent": "^7.0.2",
|
||||
|
@ -217,6 +217,7 @@ export type Note = {
|
||||
clippedCount?: number;
|
||||
poll?: {
|
||||
expiresAt: DateString | null;
|
||||
expiredAfter: DateString | null;
|
||||
multiple: boolean;
|
||||
choices: {
|
||||
isVoted: boolean;
|
||||
|
145
pnpm-lock.yaml
145
pnpm-lock.yaml
@ -1034,8 +1034,8 @@ importers:
|
||||
specifier: ^8.5.10
|
||||
version: 8.5.10
|
||||
axios:
|
||||
specifier: 1.5.0
|
||||
version: 1.5.0
|
||||
specifier: 1.6.0
|
||||
version: 1.6.0
|
||||
dayjs:
|
||||
specifier: ^1.11.10
|
||||
version: 1.11.10
|
||||
@ -2285,15 +2285,6 @@ packages:
|
||||
'@babel/helper-plugin-utils': 7.22.5
|
||||
dev: true
|
||||
|
||||
/@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.22.11):
|
||||
resolution: {integrity: sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==}
|
||||
peerDependencies:
|
||||
'@babel/core': ^7.0.0-0
|
||||
dependencies:
|
||||
'@babel/core': 7.22.11
|
||||
'@babel/helper-plugin-utils': 7.22.5
|
||||
dev: true
|
||||
|
||||
/@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.23.3):
|
||||
resolution: {integrity: sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==}
|
||||
peerDependencies:
|
||||
@ -7294,7 +7285,7 @@ packages:
|
||||
ts-dedent: 2.2.0
|
||||
type-fest: 2.19.0
|
||||
vue: 3.3.8(typescript@5.2.2)
|
||||
vue-component-type-helpers: 1.8.22
|
||||
vue-component-type-helpers: 1.8.24
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
- supports-color
|
||||
@ -7940,10 +7931,6 @@ packages:
|
||||
'@types/unist': 2.0.6
|
||||
dev: true
|
||||
|
||||
/@types/http-cache-semantics@4.0.1:
|
||||
resolution: {integrity: sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==}
|
||||
dev: false
|
||||
|
||||
/@types/http-cache-semantics@4.0.4:
|
||||
resolution: {integrity: sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==}
|
||||
|
||||
@ -9198,7 +9185,7 @@ packages:
|
||||
requiresBuild: true
|
||||
dependencies:
|
||||
delegates: 1.0.0
|
||||
readable-stream: 3.6.0
|
||||
readable-stream: 3.6.2
|
||||
dev: false
|
||||
|
||||
/arg@5.0.2:
|
||||
@ -9470,8 +9457,8 @@ packages:
|
||||
- debug
|
||||
dev: false
|
||||
|
||||
/axios@1.5.0:
|
||||
resolution: {integrity: sha512-D4DdjDo5CY50Qms0qGQTTw6Q44jl7zRwY7bthds06pUGfChBCTcQs+N743eFWGEd6pRTMd6A+I87aWyFV5wiZQ==}
|
||||
/axios@1.6.0:
|
||||
resolution: {integrity: sha512-EZ1DYihju9pwVB+jg67ogm+Tmqc6JmhamRN6I4Zt8DfZu5lbcQGw3ozH9lFejSJgs/ibaef3A9PMXPLeefFGJg==}
|
||||
dependencies:
|
||||
follow-redirects: 1.15.3(debug@4.3.4)
|
||||
form-data: 4.0.0
|
||||
@ -9501,24 +9488,6 @@ packages:
|
||||
'@babel/core': 7.22.11
|
||||
dev: true
|
||||
|
||||
/babel-jest@29.7.0(@babel/core@7.22.11):
|
||||
resolution: {integrity: sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==}
|
||||
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
||||
peerDependencies:
|
||||
'@babel/core': ^7.8.0
|
||||
dependencies:
|
||||
'@babel/core': 7.22.11
|
||||
'@jest/transform': 29.7.0
|
||||
'@types/babel__core': 7.20.0
|
||||
babel-plugin-istanbul: 6.1.1
|
||||
babel-preset-jest: 29.6.3(@babel/core@7.22.11)
|
||||
chalk: 4.1.2
|
||||
graceful-fs: 4.2.11
|
||||
slash: 3.0.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/babel-jest@29.7.0(@babel/core@7.23.3):
|
||||
resolution: {integrity: sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==}
|
||||
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
||||
@ -9596,26 +9565,6 @@ packages:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/babel-preset-current-node-syntax@1.0.1(@babel/core@7.22.11):
|
||||
resolution: {integrity: sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==}
|
||||
peerDependencies:
|
||||
'@babel/core': ^7.0.0
|
||||
dependencies:
|
||||
'@babel/core': 7.22.11
|
||||
'@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.22.11)
|
||||
'@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.22.11)
|
||||
'@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.22.11)
|
||||
'@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.22.11)
|
||||
'@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.22.11)
|
||||
'@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.22.11)
|
||||
'@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.22.11)
|
||||
'@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.22.11)
|
||||
'@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.22.11)
|
||||
'@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.22.11)
|
||||
'@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.22.11)
|
||||
'@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.22.11)
|
||||
dev: true
|
||||
|
||||
/babel-preset-current-node-syntax@1.0.1(@babel/core@7.23.3):
|
||||
resolution: {integrity: sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==}
|
||||
peerDependencies:
|
||||
@ -9636,17 +9585,6 @@ packages:
|
||||
'@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.23.3)
|
||||
dev: true
|
||||
|
||||
/babel-preset-jest@29.6.3(@babel/core@7.22.11):
|
||||
resolution: {integrity: sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==}
|
||||
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
||||
peerDependencies:
|
||||
'@babel/core': ^7.0.0
|
||||
dependencies:
|
||||
'@babel/core': 7.22.11
|
||||
babel-plugin-jest-hoist: 29.6.3
|
||||
babel-preset-current-node-syntax: 1.0.1(@babel/core@7.22.11)
|
||||
dev: true
|
||||
|
||||
/babel-preset-jest@29.6.3(@babel/core@7.23.3):
|
||||
resolution: {integrity: sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==}
|
||||
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
||||
@ -9725,7 +9663,7 @@ packages:
|
||||
dependencies:
|
||||
buffer: 5.7.1
|
||||
inherits: 2.0.4
|
||||
readable-stream: 3.6.0
|
||||
readable-stream: 3.6.2
|
||||
|
||||
/blob-util@2.0.2:
|
||||
resolution: {integrity: sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ==}
|
||||
@ -9992,10 +9930,10 @@ packages:
|
||||
resolution: {integrity: sha512-IDVO5MJ4LItE6HKFQTqT2ocAQsisOoCTUDu1ddCmnhyiwFQjXNPp4081Xj23N4tO+AFEFNzGuNEf/c8Gwwt15A==}
|
||||
engines: {node: '>=14.16'}
|
||||
dependencies:
|
||||
'@types/http-cache-semantics': 4.0.1
|
||||
'@types/http-cache-semantics': 4.0.4
|
||||
get-stream: 6.0.1
|
||||
http-cache-semantics: 4.1.1
|
||||
keyv: 4.5.2
|
||||
keyv: 4.5.4
|
||||
mimic-response: 4.0.0
|
||||
normalize-url: 8.0.0
|
||||
responselike: 3.0.0
|
||||
@ -10504,7 +10442,7 @@ packages:
|
||||
crc-32: 1.2.2
|
||||
crc32-stream: 5.0.0
|
||||
normalize-path: 3.0.0
|
||||
readable-stream: 3.6.0
|
||||
readable-stream: 3.6.2
|
||||
dev: false
|
||||
|
||||
/compressible@2.0.18:
|
||||
@ -10635,28 +10573,9 @@ packages:
|
||||
engines: {node: '>= 12.0.0'}
|
||||
dependencies:
|
||||
crc-32: 1.2.2
|
||||
readable-stream: 3.6.0
|
||||
readable-stream: 3.6.2
|
||||
dev: false
|
||||
|
||||
/create-jest@29.7.0:
|
||||
resolution: {integrity: sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==}
|
||||
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
'@jest/types': 29.6.3
|
||||
chalk: 4.1.2
|
||||
exit: 0.1.2
|
||||
graceful-fs: 4.2.11
|
||||
jest-config: 29.7.0(@types/node@20.9.4)
|
||||
jest-util: 29.7.0
|
||||
prompts: 2.4.2
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
- babel-plugin-macros
|
||||
- supports-color
|
||||
- ts-node
|
||||
dev: true
|
||||
|
||||
/create-jest@29.7.0(@types/node@20.9.1):
|
||||
resolution: {integrity: sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==}
|
||||
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
||||
@ -11091,6 +11010,7 @@ packages:
|
||||
/deepmerge@4.2.2:
|
||||
resolution: {integrity: sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
dev: false
|
||||
|
||||
/deepmerge@4.3.1:
|
||||
resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
|
||||
@ -11314,7 +11234,7 @@ packages:
|
||||
dependencies:
|
||||
end-of-stream: 1.4.4
|
||||
inherits: 2.0.4
|
||||
readable-stream: 2.3.7
|
||||
readable-stream: 2.3.8
|
||||
stream-shift: 1.0.1
|
||||
dev: true
|
||||
|
||||
@ -13951,10 +13871,10 @@ packages:
|
||||
'@jest/test-result': 29.7.0
|
||||
'@jest/types': 29.6.3
|
||||
chalk: 4.1.2
|
||||
create-jest: 29.7.0
|
||||
create-jest: 29.7.0(@types/node@20.9.1)
|
||||
exit: 0.1.2
|
||||
import-local: 3.1.0
|
||||
jest-config: 29.7.0(@types/node@20.9.4)
|
||||
jest-config: 29.7.0(@types/node@20.9.1)
|
||||
jest-util: 29.7.0
|
||||
jest-validate: 29.7.0
|
||||
yargs: 17.7.2
|
||||
@ -14005,14 +13925,14 @@ packages:
|
||||
ts-node:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@babel/core': 7.22.11
|
||||
'@babel/core': 7.23.3
|
||||
'@jest/test-sequencer': 29.7.0
|
||||
'@jest/types': 29.6.3
|
||||
'@types/node': 20.9.1
|
||||
babel-jest: 29.7.0(@babel/core@7.22.11)
|
||||
babel-jest: 29.7.0(@babel/core@7.23.3)
|
||||
chalk: 4.1.2
|
||||
ci-info: 3.7.1
|
||||
deepmerge: 4.2.2
|
||||
ci-info: 3.9.0
|
||||
deepmerge: 4.3.1
|
||||
glob: 7.2.3
|
||||
graceful-fs: 4.2.11
|
||||
jest-circus: 29.7.0
|
||||
@ -14741,12 +14661,6 @@ packages:
|
||||
safe-buffer: 5.2.1
|
||||
dev: false
|
||||
|
||||
/keyv@4.5.2:
|
||||
resolution: {integrity: sha512-5MHbFaKn8cNSmVW7BYnijeAVlE4cYA/SVkifVgrh7yotnfhKmjuXpDKjrABLnT0SfHWV21P8ow07OGfRrNDg8g==}
|
||||
dependencies:
|
||||
json-buffer: 3.0.1
|
||||
dev: false
|
||||
|
||||
/keyv@4.5.4:
|
||||
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
|
||||
dependencies:
|
||||
@ -14800,7 +14714,7 @@ packages:
|
||||
resolution: {integrity: sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==}
|
||||
engines: {node: '>= 0.6.3'}
|
||||
dependencies:
|
||||
readable-stream: 2.3.7
|
||||
readable-stream: 2.3.8
|
||||
dev: false
|
||||
|
||||
/leven@3.1.0:
|
||||
@ -17504,17 +17418,6 @@ packages:
|
||||
type-fest: 0.6.0
|
||||
dev: true
|
||||
|
||||
/readable-stream@2.3.7:
|
||||
resolution: {integrity: sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==}
|
||||
dependencies:
|
||||
core-util-is: 1.0.3
|
||||
inherits: 2.0.4
|
||||
isarray: 1.0.0
|
||||
process-nextick-args: 2.0.1
|
||||
safe-buffer: 5.1.2
|
||||
string_decoder: 1.1.1
|
||||
util-deprecate: 1.0.2
|
||||
|
||||
/readable-stream@2.3.8:
|
||||
resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==}
|
||||
dependencies:
|
||||
@ -17525,7 +17428,6 @@ packages:
|
||||
safe-buffer: 5.1.2
|
||||
string_decoder: 1.1.1
|
||||
util-deprecate: 1.0.2
|
||||
dev: true
|
||||
|
||||
/readable-stream@3.6.0:
|
||||
resolution: {integrity: sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==}
|
||||
@ -17534,6 +17436,7 @@ packages:
|
||||
inherits: 2.0.4
|
||||
string_decoder: 1.3.0
|
||||
util-deprecate: 1.0.2
|
||||
dev: false
|
||||
|
||||
/readable-stream@3.6.2:
|
||||
resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
|
||||
@ -18995,7 +18898,7 @@ packages:
|
||||
/through2@2.0.5:
|
||||
resolution: {integrity: sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==}
|
||||
dependencies:
|
||||
readable-stream: 2.3.7
|
||||
readable-stream: 2.3.8
|
||||
xtend: 4.0.2
|
||||
dev: true
|
||||
|
||||
@ -19956,8 +19859,8 @@ packages:
|
||||
/vscode-textmate@8.0.0:
|
||||
resolution: {integrity: sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg==}
|
||||
|
||||
/vue-component-type-helpers@1.8.22:
|
||||
resolution: {integrity: sha512-LK3wJHs3vJxHG292C8cnsRusgyC5SEZDCzDCD01mdE/AoREFMl2tzLRuzwyuEsOIz13tqgBcnvysN3Lxsa14Fw==}
|
||||
/vue-component-type-helpers@1.8.24:
|
||||
resolution: {integrity: sha512-lqWs/7fdRXoSBAlbouHBX+LNuaY6gI9xWW34m/ZIz9zVPYHEyw0b2/zaCBwlKx0NtKTeF/6pOpvrxVkh7nhIYg==}
|
||||
dev: true
|
||||
|
||||
/vue-component-type-helpers@1.8.4:
|
||||
|
Loading…
Reference in New Issue
Block a user