Merge branch 'develop' into no-websocket
This commit is contained in:
commit
258c5c24bd
|
@ -13,6 +13,7 @@
|
|||
- メッセージにはリアクションも可能です
|
||||
- Enhance: セキュリティを強化するため、ジョブキューのダッシュボード(bull-board)統合が削除されました。
|
||||
- Misskeyネイティブでダッシュボードを実装予定です
|
||||
- Enhance: ミュートしているユーザーをユーザー検索の結果から除外するように
|
||||
|
||||
### Client
|
||||
- Feat: 設定の管理が強化されました
|
||||
|
@ -44,6 +45,7 @@
|
|||
- 投稿フォームをリセットできるように
|
||||
- 文字数カウントを復活
|
||||
- Enhance: 2段階認証時のリカバリーコードのファイル名にサーバーURLを含めるように
|
||||
- Enhance: 全体的なブラッシュアップ
|
||||
- Fix: テーマ切り替え時に一部の色が変わらない問題を修正
|
||||
|
||||
### Server
|
||||
|
|
|
@ -2546,6 +2546,7 @@ _notification:
|
|||
newNote: "Nota nova"
|
||||
unreadAntennaNote: "Antena {name}"
|
||||
roleAssigned: "Rol assignat "
|
||||
chatRoomInvitationReceived: "T'han invitat a una sala de xat"
|
||||
emptyPushNotificationMessage: "Les notificacions han sigut actualitzades"
|
||||
achievementEarned: "Aconseguiment desblocat"
|
||||
testNotification: "Notificació de prova"
|
||||
|
@ -2574,6 +2575,7 @@ _notification:
|
|||
receiveFollowRequest: "Rebuda una petició de seguiment"
|
||||
followRequestAccepted: "Petició de seguiment acceptada"
|
||||
roleAssigned: "Rol donat"
|
||||
chatRoomInvitationReceived: "Invitat a la sala de xat"
|
||||
achievementEarned: "Assoliment desbloquejat"
|
||||
exportCompleted: "Exportació completada"
|
||||
login: "Iniciar sessió"
|
||||
|
@ -2713,6 +2715,7 @@ _moderationLogTypes:
|
|||
deletePage: "Esborrar la pàgina"
|
||||
deleteFlash: "Esborrar el guió"
|
||||
deleteGalleryPost: "Esborrar la publicació de la galeria"
|
||||
deleteChatRoom: "Esborra la sala de xat"
|
||||
updateProxyAccountDescription: "Actualitzar descripció del compte proxy"
|
||||
_fileViewer:
|
||||
title: "Detall del fitxer"
|
||||
|
|
|
@ -11,6 +11,7 @@ username: "Uživatelské jméno"
|
|||
password: "Heslo"
|
||||
initialPasswordForSetup: "Počáteční heslo pro nastavení"
|
||||
initialPasswordIsIncorrect: "Počáteční heslo pro nastavení je nesprávné"
|
||||
initialPasswordForSetupDescription: "Použijte heslo, které jste nastavili v konfiguračním souboru, pokud jste Misskey instalovali ručně.\nPokud užíváte Misskey hostovací službu, použijte poskytnuté heslo.\nPokud jste heslo nenastavovali, zanechte prázdné."
|
||||
forgotPassword: "Zapomenuté heslo"
|
||||
fetchingAsApObject: "Načítám data z Fediversu..."
|
||||
ok: "Potvrdit"
|
||||
|
@ -48,6 +49,8 @@ pin: "Připnout"
|
|||
unpin: "Odepnout"
|
||||
copyContent: "Zkopírovat obsah"
|
||||
copyLink: "Kopírovat odkaz"
|
||||
copyRemoteLink: "Zkoprírovat vzdálený odkaz"
|
||||
copyLinkRenote: "Zkopírovat odkaz renotu"
|
||||
delete: "Smazat"
|
||||
deleteAndEdit: "Smazat a upravit"
|
||||
deleteAndEditConfirm: "Jste si jistí že chcete smazat tuto poznámku a editovat ji? Ztratíte tím všechny reakce, sdílení a odpovědi na ni."
|
||||
|
@ -540,6 +543,7 @@ showInPage: "Zobrazit na stránce"
|
|||
popout: "Pop-out"
|
||||
volume: "Hlasitost"
|
||||
masterVolume: "Celková hlasitost"
|
||||
notUseSound: "Zakázat zvuk"
|
||||
details: "Detaily"
|
||||
chooseEmoji: "Vybrat emotikon"
|
||||
unableToProcess: "Operace nebyla dokončena."
|
||||
|
|
|
@ -5358,6 +5358,10 @@ export interface Locale extends ILocale {
|
|||
* 通常これは自動で行われていますが、何らかの理由により上手く移行されなかった場合は手動で移行処理をトリガーできます。現在の設定情報は上書きされます。
|
||||
*/
|
||||
"migrateOldSettings_description": string;
|
||||
/**
|
||||
* 圧縮
|
||||
*/
|
||||
"compress": string;
|
||||
"_chat": {
|
||||
/**
|
||||
* まだメッセージはありません
|
||||
|
|
|
@ -301,6 +301,7 @@ uploadFromUrlMayTakeTime: "Il caricamento del file può richiedere tempo."
|
|||
explore: "Esplora"
|
||||
messageRead: "Visualizzato"
|
||||
noMoreHistory: "Non c'è più cronologia da visualizzare"
|
||||
startChat: "Inizia a chattare"
|
||||
nUsersRead: "Letto da {n} persone"
|
||||
agreeTo: "Sono d'accordo con {0}"
|
||||
agree: "Accetto"
|
||||
|
@ -1331,12 +1332,52 @@ emojiPalette: "Tavolozza emoji"
|
|||
postForm: "Finestra di pubblicazione"
|
||||
textCount: "Il numero di caratteri"
|
||||
information: "Informazioni"
|
||||
chat: "Chat"
|
||||
migrateOldSettings: "Migrare le vecchie impostazioni"
|
||||
migrateOldSettings_description: "Di solito, viene fatto automaticamente. Se per qualche motivo non fossero migrate con successo, è possibile avviare il processo di migrazione manualmente, sovrascrivendo le configurazioni attuali."
|
||||
_chat:
|
||||
noMessagesYet: "Ancora nessun messaggio"
|
||||
newMessage: "Nuovo messaggio"
|
||||
individualChat: "Chat individuale"
|
||||
individualChat_description: "Puoi chattare con una persona specifica."
|
||||
roomChat: "Stanza di chat"
|
||||
roomChat_description: "Puoi chattare con più persone.\nInoltre, anche le persone che non consentono chat personalizzate possono chattare se gli altri accettano."
|
||||
createRoom: "Crea stanza"
|
||||
inviteUserToChat: "Invita a chattare altre persone"
|
||||
yourRooms: "Le tue stanze"
|
||||
joiningRooms: "Stanze a cui partecipi"
|
||||
invitations: "Invita"
|
||||
noInvitations: "Nessun invito"
|
||||
history: "Cronologia"
|
||||
noHistory: "Nessuna cronologia"
|
||||
noRooms: "Nessuna stanza"
|
||||
inviteUser: "Invita"
|
||||
sentInvitations: "Inviti spediti"
|
||||
join: "Entra"
|
||||
ignore: "Ignora"
|
||||
leave: "Esci"
|
||||
members: "Membri"
|
||||
searchMessages: "Cerca messaggi"
|
||||
home: "Home"
|
||||
send: "Inviare"
|
||||
newline: "Nuova riga"
|
||||
muteThisRoom: "Silenzia stanza"
|
||||
deleteRoom: "Elimina stanza"
|
||||
cannotChatWithTheUser: "Impossibile chattare con questa persona"
|
||||
cannotChatWithTheUser_description: "La chat potrebbe non essere disponibile, oppure l'altra persona potrebbe non esserlo."
|
||||
chatWithThisUser: "Chatta con questa persona"
|
||||
thisUserAllowsChatOnlyFromFollowers: "Questa persona permette di chattare soltanto i propri Follower."
|
||||
thisUserAllowsChatOnlyFromFollowing: "Questa persona permette di chattare soltanto ai suoi Follow."
|
||||
thisUserAllowsChatOnlyFromMutualFollowing: "Questa persona permette di chattare solo a relazioni reciproche."
|
||||
thisUserNotAllowedChatAnyone: "Questa persona non permette di chattare a nessuno."
|
||||
chatAllowedUsers: "Persone ammesse alla chat"
|
||||
chatAllowedUsers_note: "Puoi chattare con le persone a cui hai già inviato un messaggio, indipendentemente da questa impostazione."
|
||||
_chatAllowedUsers:
|
||||
everyone: "Chiunque"
|
||||
followers: "Solo i tuoi Follower"
|
||||
following: "Solo i tuoi Follow"
|
||||
mutual: "Solo relazioni reciproche"
|
||||
none: "Nessuno"
|
||||
_emojiPalette:
|
||||
palettes: "Tavolozza"
|
||||
enableSyncBetweenDevicesForPalettes: "Attiva la sincronizzazione tra dispositivi"
|
||||
|
@ -1362,6 +1403,12 @@ _settings:
|
|||
timelineAndNote: "Note e Timeline"
|
||||
makeEveryTextElementsSelectable: "Imposta ogni elemento come selezionabile"
|
||||
makeEveryTextElementsSelectable_description: "Potrebbe ridurre l'usabilità in alcune situazioni."
|
||||
showNavbarSubButtons: "Mostra i pulsanti secondari nella barra di navigazione"
|
||||
ifOn: "Quando attivato"
|
||||
ifOff: "Quando disattivato"
|
||||
_chat:
|
||||
showSenderName: "Mostra il nome del mittente"
|
||||
sendOnEnter: "Invio spedisce"
|
||||
_preferencesProfile:
|
||||
profileName: "Nome del profilo"
|
||||
profileNameDescription: "Impostare il nome che indentifica questo dispositivo."
|
||||
|
@ -1871,6 +1918,7 @@ _role:
|
|||
canImportFollowing: "Può importare Following"
|
||||
canImportMuting: "Può importare Silenziati"
|
||||
canImportUserLists: "Può importare liste di Profili"
|
||||
canChat: "Chat consentita"
|
||||
_condition:
|
||||
roleAssignedTo: "Assegnato a ruoli manualmente"
|
||||
isLocal: "Profilo locale"
|
||||
|
@ -2101,6 +2149,7 @@ _sfx:
|
|||
noteMy: "Mia nota"
|
||||
notification: "Notifiche"
|
||||
reaction: "Quando seleziono una reazione"
|
||||
chatMessage: "Messaggio di chat"
|
||||
_soundSettings:
|
||||
driveFile: "Suoni del Drive"
|
||||
driveFileWarn: "Seleziona file dal dispositivo"
|
||||
|
@ -2248,6 +2297,7 @@ _permissions:
|
|||
"read:federation": "Vedere la federazione"
|
||||
"write:report-abuse": "Inviare segnalazioni"
|
||||
"write:chat": "Gestire la chat"
|
||||
"read:chat": "Visualizzare le chat"
|
||||
_auth:
|
||||
shareAccessTitle: "Permessi dell'applicazione"
|
||||
shareAccess: "Vuoi autorizzare {name} ad accedere al tuo profilo?"
|
||||
|
@ -2496,6 +2546,7 @@ _notification:
|
|||
newNote: "Nuove Note"
|
||||
unreadAntennaNote: "Antenna {name}"
|
||||
roleAssigned: "Ruolo assegnato"
|
||||
chatRoomInvitationReceived: "Invito in una stanza di chat"
|
||||
emptyPushNotificationMessage: "Le notifiche push sono state aggiornate."
|
||||
achievementEarned: "Obiettivo raggiunto"
|
||||
testNotification: "Provare la notifica"
|
||||
|
@ -2524,6 +2575,7 @@ _notification:
|
|||
receiveFollowRequest: "Richieste di follow in arrivo"
|
||||
followRequestAccepted: "Richieste di follow accettate"
|
||||
roleAssigned: "Ruolo concesso"
|
||||
chatRoomInvitationReceived: "Invito in una stanza di chat"
|
||||
achievementEarned: "Risultato raggiunto"
|
||||
exportCompleted: "Esportazione completata"
|
||||
login: "Accessi"
|
||||
|
@ -2663,6 +2715,7 @@ _moderationLogTypes:
|
|||
deletePage: "Pagina eliminata"
|
||||
deleteFlash: "Play eliminato"
|
||||
deleteGalleryPost: "Eliminazione pubblicazione nella Galleria"
|
||||
deleteChatRoom: "Elimina chat"
|
||||
updateProxyAccountDescription: "Aggiornata la descrizione del profilo proxy"
|
||||
_fileViewer:
|
||||
title: "Dettagli del file"
|
||||
|
|
|
@ -1335,6 +1335,7 @@ information: "情報"
|
|||
chat: "チャット"
|
||||
migrateOldSettings: "旧設定情報を移行"
|
||||
migrateOldSettings_description: "通常これは自動で行われていますが、何らかの理由により上手く移行されなかった場合は手動で移行処理をトリガーできます。現在の設定情報は上書きされます。"
|
||||
compress: "圧縮"
|
||||
|
||||
_chat:
|
||||
noMessagesYet: "まだメッセージはありません"
|
||||
|
|
|
@ -2546,6 +2546,7 @@ _notification:
|
|||
newNote: "新的帖子"
|
||||
unreadAntennaNote: "天线 {name}"
|
||||
roleAssigned: "授予的角色"
|
||||
chatRoomInvitationReceived: "受邀加入聊天室"
|
||||
emptyPushNotificationMessage: "推送通知已更新"
|
||||
achievementEarned: "获得成就"
|
||||
testNotification: "测试通知"
|
||||
|
@ -2574,6 +2575,7 @@ _notification:
|
|||
receiveFollowRequest: "收到关注请求"
|
||||
followRequestAccepted: "关注请求已通过"
|
||||
roleAssigned: "授予的角色"
|
||||
chatRoomInvitationReceived: "受邀加入聊天室"
|
||||
achievementEarned: "取得的成就"
|
||||
exportCompleted: "已完成导出"
|
||||
login: "登录"
|
||||
|
@ -2713,6 +2715,7 @@ _moderationLogTypes:
|
|||
deletePage: "删除了页面"
|
||||
deleteFlash: "删除了 Play"
|
||||
deleteGalleryPost: "删除了图库稿件"
|
||||
deleteChatRoom: "删除聊天室"
|
||||
updateProxyAccountDescription: "更新代理账户的简介"
|
||||
_fileViewer:
|
||||
title: "文件信息"
|
||||
|
|
|
@ -1350,7 +1350,7 @@ _chat:
|
|||
noInvitations: "沒有邀請"
|
||||
history: "歷史紀錄"
|
||||
noHistory: "沒有歷史紀錄"
|
||||
noRooms: "此聊天室不存在"
|
||||
noRooms: "沒有可用的聊天室"
|
||||
inviteUser: "邀請使用者"
|
||||
sentInvitations: "已傳送的邀請"
|
||||
join: "加入"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "misskey",
|
||||
"version": "2025.3.2-beta.13",
|
||||
"version": "2025.3.2-beta.14",
|
||||
"codename": "nasubi",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
|
@ -69,7 +69,7 @@ export class QueryService {
|
|||
|
||||
// ここでいうBlockedは被Blockedの意
|
||||
@bindThis
|
||||
public generateBlockedUserQuery(q: SelectQueryBuilder<any>, me: { id: MiUser['id'] }): void {
|
||||
public generateBlockedUserQueryForNotes(q: SelectQueryBuilder<any>, me: { id: MiUser['id'] }): void {
|
||||
const blockingQuery = this.blockingsRepository.createQueryBuilder('blocking')
|
||||
.select('blocking.blockerId')
|
||||
.where('blocking.blockeeId = :blockeeId', { blockeeId: me.id });
|
||||
|
@ -127,7 +127,7 @@ export class QueryService {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
public generateMutedUserQuery(q: SelectQueryBuilder<any>, me: { id: MiUser['id'] }, exclude?: { id: MiUser['id'] }): void {
|
||||
public generateMutedUserQueryForNotes(q: SelectQueryBuilder<any>, me: { id: MiUser['id'] }, exclude?: { id: MiUser['id'] }): void {
|
||||
const mutingQuery = this.mutingsRepository.createQueryBuilder('muting')
|
||||
.select('muting.muteeId')
|
||||
.where('muting.muterId = :muterId', { muterId: me.id });
|
||||
|
|
|
@ -234,8 +234,8 @@ export class SearchService {
|
|||
}
|
||||
|
||||
this.queryService.generateVisibilityQuery(query, me);
|
||||
if (me) this.queryService.generateMutedUserQuery(query, me);
|
||||
if (me) this.queryService.generateBlockedUserQuery(query, me);
|
||||
if (me) this.queryService.generateMutedUserQueryForNotes(query, me);
|
||||
if (me) this.queryService.generateBlockedUserQueryForNotes(query, me);
|
||||
|
||||
return query.limit(pagination.limit).getMany();
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Brackets, SelectQueryBuilder } from 'typeorm';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { type FollowingsRepository, MiUser, type UsersRepository } from '@/models/_.js';
|
||||
import { type FollowingsRepository, MiUser, type MutingsRepository, type UserProfilesRepository, type UsersRepository } from '@/models/_.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { sqlLikeEscape } from '@/misc/sql-like-escape.js';
|
||||
import type { Config } from '@/config.js';
|
||||
|
@ -22,10 +22,19 @@ export class UserSearchService {
|
|||
constructor(
|
||||
@Inject(DI.config)
|
||||
private config: Config,
|
||||
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
||||
@Inject(DI.userProfilesRepository)
|
||||
private userProfilesRepository: UserProfilesRepository,
|
||||
|
||||
@Inject(DI.followingsRepository)
|
||||
private followingsRepository: FollowingsRepository,
|
||||
|
||||
@Inject(DI.mutingsRepository)
|
||||
private mutingsRepository: MutingsRepository,
|
||||
|
||||
private userEntityService: UserEntityService,
|
||||
) {
|
||||
}
|
||||
|
@ -58,7 +67,7 @@ export class UserSearchService {
|
|||
* @see {@link UserSearchService#buildSearchUserNoLoginQueries}
|
||||
*/
|
||||
@bindThis
|
||||
public async search(
|
||||
public async searchByUsernameAndHost(
|
||||
params: {
|
||||
username?: string | null,
|
||||
host?: string | null,
|
||||
|
@ -202,4 +211,91 @@ export class UserSearchService {
|
|||
|
||||
return userQuery;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async search(query: string, meId: MiUser['id'] | null, options: Partial<{
|
||||
limit: number;
|
||||
offset: number;
|
||||
origin: 'local' | 'remote' | 'combined';
|
||||
}> = {}) {
|
||||
const activeThreshold = new Date(Date.now() - (1000 * 60 * 60 * 24 * 30)); // 30日
|
||||
|
||||
const isUsername = query.startsWith('@') && !query.includes(' ') && query.indexOf('@', 1) === -1;
|
||||
|
||||
let users: MiUser[] = [];
|
||||
|
||||
const mutingQuery = meId == null ? null : this.mutingsRepository.createQueryBuilder('muting')
|
||||
.select('muting.muteeId')
|
||||
.where('muting.muterId = :muterId', { muterId: meId });
|
||||
|
||||
const nameQuery = this.usersRepository.createQueryBuilder('user')
|
||||
.where(new Brackets(qb => {
|
||||
qb.where('user.name ILIKE :query', { query: '%' + sqlLikeEscape(query) + '%' });
|
||||
|
||||
if (isUsername) {
|
||||
qb.orWhere('user.usernameLower LIKE :username', { username: sqlLikeEscape(query.replace('@', '').toLowerCase()) + '%' });
|
||||
} else if (this.userEntityService.validateLocalUsername(query)) { // Also search username if it qualifies as username
|
||||
qb.orWhere('user.usernameLower LIKE :username', { username: '%' + sqlLikeEscape(query.toLowerCase()) + '%' });
|
||||
}
|
||||
}))
|
||||
.andWhere(new Brackets(qb => {
|
||||
qb
|
||||
.where('user.updatedAt IS NULL')
|
||||
.orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold });
|
||||
}))
|
||||
.andWhere('user.isSuspended = FALSE');
|
||||
|
||||
if (mutingQuery) {
|
||||
nameQuery.andWhere(`user.id NOT IN (${mutingQuery.getQuery()})`);
|
||||
nameQuery.setParameters(mutingQuery.getParameters());
|
||||
}
|
||||
|
||||
if (options.origin === 'local') {
|
||||
nameQuery.andWhere('user.host IS NULL');
|
||||
} else if (options.origin === 'remote') {
|
||||
nameQuery.andWhere('user.host IS NOT NULL');
|
||||
}
|
||||
|
||||
users = await nameQuery
|
||||
.orderBy('user.updatedAt', 'DESC', 'NULLS LAST')
|
||||
.limit(options.limit)
|
||||
.offset(options.offset)
|
||||
.getMany();
|
||||
|
||||
if (users.length < (options.limit ?? 30)) {
|
||||
const profQuery = this.userProfilesRepository.createQueryBuilder('prof')
|
||||
.select('prof.userId')
|
||||
.where('prof.description ILIKE :query', { query: '%' + sqlLikeEscape(query) + '%' });
|
||||
|
||||
if (mutingQuery) {
|
||||
profQuery.andWhere(`prof.userId NOT IN (${mutingQuery.getQuery()})`);
|
||||
profQuery.setParameters(mutingQuery.getParameters());
|
||||
}
|
||||
|
||||
if (options.origin === 'local') {
|
||||
profQuery.andWhere('prof.userHost IS NULL');
|
||||
} else if (options.origin === 'remote') {
|
||||
profQuery.andWhere('prof.userHost IS NOT NULL');
|
||||
}
|
||||
|
||||
const userQuery = this.usersRepository.createQueryBuilder('user')
|
||||
.where(`user.id IN (${ profQuery.getQuery() })`)
|
||||
.andWhere(new Brackets(qb => {
|
||||
qb
|
||||
.where('user.updatedAt IS NULL')
|
||||
.orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold });
|
||||
}))
|
||||
.andWhere('user.isSuspended = FALSE')
|
||||
.setParameters(profQuery.getParameters());
|
||||
|
||||
users = users.concat(await userQuery
|
||||
.orderBy('user.updatedAt', 'DESC', 'NULLS LAST')
|
||||
.limit(options.limit)
|
||||
.offset(options.offset)
|
||||
.getMany(),
|
||||
);
|
||||
}
|
||||
|
||||
return users;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -109,8 +109,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
.leftJoinAndSelect('renote.user', 'renoteUser');
|
||||
|
||||
this.queryService.generateVisibilityQuery(query, me);
|
||||
this.queryService.generateMutedUserQuery(query, me);
|
||||
this.queryService.generateBlockedUserQuery(query, me);
|
||||
this.queryService.generateMutedUserQueryForNotes(query, me);
|
||||
this.queryService.generateBlockedUserQueryForNotes(query, me);
|
||||
|
||||
const notes = await query.getMany();
|
||||
if (sinceId != null && untilId == null) {
|
||||
|
|
|
@ -122,8 +122,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
.leftJoinAndSelect('note.channel', 'channel');
|
||||
|
||||
if (me) {
|
||||
this.queryService.generateMutedUserQuery(query, me);
|
||||
this.queryService.generateBlockedUserQuery(query, me);
|
||||
this.queryService.generateMutedUserQueryForNotes(query, me);
|
||||
this.queryService.generateBlockedUserQueryForNotes(query, me);
|
||||
}
|
||||
//#endregion
|
||||
|
||||
|
|
|
@ -87,8 +87,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
|
||||
if (me) {
|
||||
this.queryService.generateVisibilityQuery(query, me);
|
||||
this.queryService.generateMutedUserQuery(query, me);
|
||||
this.queryService.generateBlockedUserQuery(query, me);
|
||||
this.queryService.generateMutedUserQueryForNotes(query, me);
|
||||
this.queryService.generateBlockedUserQueryForNotes(query, me);
|
||||
}
|
||||
|
||||
const notes = await query
|
||||
|
|
|
@ -71,8 +71,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
|
||||
this.queryService.generateVisibilityQuery(query, me);
|
||||
if (me) {
|
||||
this.queryService.generateMutedUserQuery(query, me);
|
||||
this.queryService.generateBlockedUserQuery(query, me);
|
||||
this.queryService.generateMutedUserQueryForNotes(query, me);
|
||||
this.queryService.generateBlockedUserQueryForNotes(query, me);
|
||||
}
|
||||
|
||||
const notes = await query.limit(ps.limit).getMany();
|
||||
|
|
|
@ -79,8 +79,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
.leftJoinAndSelect('renote.user', 'renoteUser');
|
||||
|
||||
if (me) {
|
||||
this.queryService.generateMutedUserQuery(query, me);
|
||||
this.queryService.generateBlockedUserQuery(query, me);
|
||||
this.queryService.generateMutedUserQueryForNotes(query, me);
|
||||
this.queryService.generateBlockedUserQueryForNotes(query, me);
|
||||
this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
|
||||
}
|
||||
|
||||
|
|
|
@ -243,8 +243,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
}
|
||||
|
||||
this.queryService.generateVisibilityQuery(query, me);
|
||||
this.queryService.generateMutedUserQuery(query, me);
|
||||
this.queryService.generateBlockedUserQuery(query, me);
|
||||
this.queryService.generateMutedUserQueryForNotes(query, me);
|
||||
this.queryService.generateBlockedUserQueryForNotes(query, me);
|
||||
this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
|
||||
|
||||
if (ps.includeMyRenotes === false) {
|
||||
|
|
|
@ -156,8 +156,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
.leftJoinAndSelect('renote.user', 'renoteUser');
|
||||
|
||||
this.queryService.generateVisibilityQuery(query, me);
|
||||
if (me) this.queryService.generateMutedUserQuery(query, me);
|
||||
if (me) this.queryService.generateBlockedUserQuery(query, me);
|
||||
if (me) this.queryService.generateMutedUserQueryForNotes(query, me);
|
||||
if (me) this.queryService.generateBlockedUserQueryForNotes(query, me);
|
||||
if (me) this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
|
||||
|
||||
if (ps.withFiles) {
|
||||
|
|
|
@ -72,9 +72,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
.leftJoinAndSelect('renote.user', 'renoteUser');
|
||||
|
||||
this.queryService.generateVisibilityQuery(query, me);
|
||||
this.queryService.generateMutedUserQuery(query, me);
|
||||
this.queryService.generateMutedUserQueryForNotes(query, me);
|
||||
this.queryService.generateMutedNoteThreadQuery(query, me);
|
||||
this.queryService.generateBlockedUserQuery(query, me);
|
||||
this.queryService.generateBlockedUserQueryForNotes(query, me);
|
||||
|
||||
if (ps.visibility) {
|
||||
query.andWhere('note.visibility = :visibility', { visibility: ps.visibility });
|
||||
|
|
|
@ -72,8 +72,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
.leftJoinAndSelect('renote.user', 'renoteUser');
|
||||
|
||||
this.queryService.generateVisibilityQuery(query, me);
|
||||
if (me) this.queryService.generateMutedUserQuery(query, me);
|
||||
if (me) this.queryService.generateBlockedUserQuery(query, me);
|
||||
if (me) this.queryService.generateMutedUserQueryForNotes(query, me);
|
||||
if (me) this.queryService.generateBlockedUserQueryForNotes(query, me);
|
||||
|
||||
const renotes = await query.limit(ps.limit).getMany();
|
||||
|
||||
|
|
|
@ -56,8 +56,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
.leftJoinAndSelect('renote.user', 'renoteUser');
|
||||
|
||||
this.queryService.generateVisibilityQuery(query, me);
|
||||
if (me) this.queryService.generateMutedUserQuery(query, me);
|
||||
if (me) this.queryService.generateBlockedUserQuery(query, me);
|
||||
if (me) this.queryService.generateMutedUserQueryForNotes(query, me);
|
||||
if (me) this.queryService.generateBlockedUserQueryForNotes(query, me);
|
||||
|
||||
const timeline = await query.limit(ps.limit).getMany();
|
||||
|
||||
|
|
|
@ -81,8 +81,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
.leftJoinAndSelect('renote.user', 'renoteUser');
|
||||
|
||||
this.queryService.generateVisibilityQuery(query, me);
|
||||
if (me) this.queryService.generateMutedUserQuery(query, me);
|
||||
if (me) this.queryService.generateBlockedUserQuery(query, me);
|
||||
if (me) this.queryService.generateMutedUserQueryForNotes(query, me);
|
||||
if (me) this.queryService.generateBlockedUserQueryForNotes(query, me);
|
||||
|
||||
try {
|
||||
if (ps.tag) {
|
||||
|
|
|
@ -199,8 +199,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
}));
|
||||
|
||||
this.queryService.generateVisibilityQuery(query, me);
|
||||
this.queryService.generateMutedUserQuery(query, me);
|
||||
this.queryService.generateBlockedUserQuery(query, me);
|
||||
this.queryService.generateMutedUserQueryForNotes(query, me);
|
||||
this.queryService.generateBlockedUserQueryForNotes(query, me);
|
||||
this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
|
||||
|
||||
if (ps.includeMyRenotes === false) {
|
||||
|
|
|
@ -184,8 +184,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
}));
|
||||
|
||||
this.queryService.generateVisibilityQuery(query, me);
|
||||
this.queryService.generateMutedUserQuery(query, me);
|
||||
this.queryService.generateBlockedUserQuery(query, me);
|
||||
this.queryService.generateMutedUserQueryForNotes(query, me);
|
||||
this.queryService.generateBlockedUserQueryForNotes(query, me);
|
||||
this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
|
||||
|
||||
if (ps.includeMyRenotes === false) {
|
||||
|
|
|
@ -102,8 +102,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
.leftJoinAndSelect('renote.user', 'renoteUser');
|
||||
|
||||
this.queryService.generateVisibilityQuery(query, me);
|
||||
this.queryService.generateMutedUserQuery(query, me);
|
||||
this.queryService.generateBlockedUserQuery(query, me);
|
||||
this.queryService.generateMutedUserQueryForNotes(query, me);
|
||||
this.queryService.generateBlockedUserQueryForNotes(query, me);
|
||||
|
||||
const notes = await query.getMany();
|
||||
notes.sort((a, b) => a.id > b.id ? -1 : 1);
|
||||
|
|
|
@ -185,8 +185,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
|
||||
this.queryService.generateVisibilityQuery(query, me);
|
||||
if (me) {
|
||||
this.queryService.generateMutedUserQuery(query, me, { id: ps.userId });
|
||||
this.queryService.generateBlockedUserQuery(query, me);
|
||||
this.queryService.generateMutedUserQueryForNotes(query, me, { id: ps.userId });
|
||||
this.queryService.generateBlockedUserQueryForNotes(query, me);
|
||||
}
|
||||
|
||||
if (ps.withFiles) {
|
||||
|
|
|
@ -63,7 +63,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
|
||||
this.queryService.generateMutedUserQueryForUsers(query, me);
|
||||
this.queryService.generateBlockQueryForUsers(query, me);
|
||||
this.queryService.generateBlockedUserQuery(query, me);
|
||||
this.queryService.generateBlockedUserQueryForNotes(query, me);
|
||||
|
||||
const followingQuery = this.followingsRepository.createQueryBuilder('following')
|
||||
.select('following.followeeId')
|
||||
|
|
|
@ -46,7 +46,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
private userSearchService: UserSearchService,
|
||||
) {
|
||||
super(meta, paramDef, (ps, me) => {
|
||||
return this.userSearchService.search({
|
||||
return this.userSearchService.searchByUsernameAndHost({
|
||||
username: ps.username,
|
||||
host: ps.host,
|
||||
}, {
|
||||
|
|
|
@ -3,14 +3,11 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Brackets } from 'typeorm';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import type { UsersRepository, UserProfilesRepository } from '@/models/_.js';
|
||||
import type { MiUser } from '@/models/User.js';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { sqlLikeEscape } from '@/misc/sql-like-escape.js';
|
||||
import { UserSearchService } from '@/core/UserSearchService.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['users'],
|
||||
|
@ -45,79 +42,15 @@ export const paramDef = {
|
|||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
||||
@Inject(DI.userProfilesRepository)
|
||||
private userProfilesRepository: UserProfilesRepository,
|
||||
|
||||
private userEntityService: UserEntityService,
|
||||
private userSearchService: UserSearchService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const activeThreshold = new Date(Date.now() - (1000 * 60 * 60 * 24 * 30)); // 30日
|
||||
|
||||
ps.query = ps.query.trim();
|
||||
const isUsername = ps.query.startsWith('@') && !ps.query.includes(' ') && ps.query.indexOf('@', 1) === -1;
|
||||
|
||||
let users: MiUser[] = [];
|
||||
|
||||
const nameQuery = this.usersRepository.createQueryBuilder('user')
|
||||
.where(new Brackets(qb => {
|
||||
qb.where('user.name ILIKE :query', { query: '%' + sqlLikeEscape(ps.query) + '%' });
|
||||
|
||||
if (isUsername) {
|
||||
qb.orWhere('user.usernameLower LIKE :username', { username: sqlLikeEscape(ps.query.replace('@', '').toLowerCase()) + '%' });
|
||||
} else if (this.userEntityService.validateLocalUsername(ps.query)) { // Also search username if it qualifies as username
|
||||
qb.orWhere('user.usernameLower LIKE :username', { username: '%' + sqlLikeEscape(ps.query.toLowerCase()) + '%' });
|
||||
}
|
||||
}))
|
||||
.andWhere(new Brackets(qb => {
|
||||
qb
|
||||
.where('user.updatedAt IS NULL')
|
||||
.orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold });
|
||||
}))
|
||||
.andWhere('user.isSuspended = FALSE');
|
||||
|
||||
if (ps.origin === 'local') {
|
||||
nameQuery.andWhere('user.host IS NULL');
|
||||
} else if (ps.origin === 'remote') {
|
||||
nameQuery.andWhere('user.host IS NOT NULL');
|
||||
}
|
||||
|
||||
users = await nameQuery
|
||||
.orderBy('user.updatedAt', 'DESC', 'NULLS LAST')
|
||||
.limit(ps.limit)
|
||||
.offset(ps.offset)
|
||||
.getMany();
|
||||
|
||||
if (users.length < ps.limit) {
|
||||
const profQuery = this.userProfilesRepository.createQueryBuilder('prof')
|
||||
.select('prof.userId')
|
||||
.where('prof.description ILIKE :query', { query: '%' + sqlLikeEscape(ps.query) + '%' });
|
||||
|
||||
if (ps.origin === 'local') {
|
||||
profQuery.andWhere('prof.userHost IS NULL');
|
||||
} else if (ps.origin === 'remote') {
|
||||
profQuery.andWhere('prof.userHost IS NOT NULL');
|
||||
}
|
||||
|
||||
const query = this.usersRepository.createQueryBuilder('user')
|
||||
.where(`user.id IN (${ profQuery.getQuery() })`)
|
||||
.andWhere(new Brackets(qb => {
|
||||
qb
|
||||
.where('user.updatedAt IS NULL')
|
||||
.orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold });
|
||||
}))
|
||||
.andWhere('user.isSuspended = FALSE')
|
||||
.setParameters(profQuery.getParameters());
|
||||
|
||||
users = users.concat(await query
|
||||
.orderBy('user.updatedAt', 'DESC', 'NULLS LAST')
|
||||
.limit(ps.limit)
|
||||
.offset(ps.offset)
|
||||
.getMany(),
|
||||
);
|
||||
}
|
||||
const users = await this.userSearchService.search(ps.query.trim(), me?.id ?? null, {
|
||||
offset: ps.offset,
|
||||
limit: ps.limit,
|
||||
origin: ps.origin,
|
||||
});
|
||||
|
||||
return await this.userEntityService.packMany(users, me, { schema: ps.detail ? 'UserDetailed' : 'UserLite' });
|
||||
});
|
||||
|
|
|
@ -732,7 +732,7 @@ describe('ユーザー', () => {
|
|||
});
|
||||
test.each([
|
||||
{ label: '「見つけやすくする」がOFFのユーザーが含まれる', user: () => userNotExplorable },
|
||||
{ label: 'ミュートユーザーが含まれる', user: () => userMutedByAlice },
|
||||
{ label: 'ミュートユーザーが含まれない', user: () => userMutedByAlice, excluded: true },
|
||||
{ label: 'ブロックされているユーザーが含まれる', user: () => userBlockedByAlice },
|
||||
{ label: 'ブロックしてきているユーザーが含まれる', user: () => userBlockingAlice },
|
||||
{ label: '承認制ユーザーが含まれる', user: () => userLocking },
|
||||
|
|
|
@ -134,13 +134,13 @@ describe('UserSearchService', () => {
|
|||
await app.close();
|
||||
});
|
||||
|
||||
describe('search', () => {
|
||||
describe('searchByUsernameAndHost', () => {
|
||||
test('フォロー中のアクティブユーザのうち、"al"から始まる人が全員ヒットする', async () => {
|
||||
await createFollowings(root, [alice, alyce, alycia, alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]);
|
||||
await setActive([alice, alyce, alyssa, bob, bobbi, bobbie, bobby]);
|
||||
await setInactive([alycia, alysha, alyson]);
|
||||
|
||||
const result = await service.search(
|
||||
const result = await service.searchByUsernameAndHost(
|
||||
{ username: 'al' },
|
||||
{ limit: 100 },
|
||||
root,
|
||||
|
@ -154,7 +154,7 @@ describe('UserSearchService', () => {
|
|||
await createFollowings(root, [alycia, alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]);
|
||||
await setInactive([alice, alyce, alycia, alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]);
|
||||
|
||||
const result = await service.search(
|
||||
const result = await service.searchByUsernameAndHost(
|
||||
{ username: 'al' },
|
||||
{ limit: 100 },
|
||||
root,
|
||||
|
@ -168,7 +168,7 @@ describe('UserSearchService', () => {
|
|||
await setActive([alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]);
|
||||
await setInactive([alice, alyce, alycia]);
|
||||
|
||||
const result = await service.search(
|
||||
const result = await service.searchByUsernameAndHost(
|
||||
{ username: 'al' },
|
||||
{ limit: 100 },
|
||||
root,
|
||||
|
@ -181,7 +181,7 @@ describe('UserSearchService', () => {
|
|||
test('フォローしていない非アクティブユーザのうち、"al"から始まる人が全員ヒットする', async () => {
|
||||
await setInactive([alice, alyce, alycia, alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]);
|
||||
|
||||
const result = await service.search(
|
||||
const result = await service.searchByUsernameAndHost(
|
||||
{ username: 'al' },
|
||||
{ limit: 100 },
|
||||
root,
|
||||
|
@ -195,7 +195,7 @@ describe('UserSearchService', () => {
|
|||
await setActive([root, alyssa, bob, bobbi, alyce, alycia]);
|
||||
await setInactive([alyson, alice, alysha, bobbie, bobby]);
|
||||
|
||||
const result = await service.search(
|
||||
const result = await service.searchByUsernameAndHost(
|
||||
{ },
|
||||
{ limit: 100 },
|
||||
root,
|
||||
|
@ -216,7 +216,7 @@ describe('UserSearchService', () => {
|
|||
await setActive([alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]);
|
||||
await setInactive([alice, alyce, alycia]);
|
||||
|
||||
const result = await service.search(
|
||||
const result = await service.searchByUsernameAndHost(
|
||||
{ username: 'al' },
|
||||
{ limit: 100 },
|
||||
);
|
||||
|
@ -228,7 +228,7 @@ describe('UserSearchService', () => {
|
|||
test('[非ログイン] 非アクティブユーザのうち、"al"から始まる人が全員ヒットする', async () => {
|
||||
await setInactive([alice, alyce, alycia, alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]);
|
||||
|
||||
const result = await service.search(
|
||||
const result = await service.searchByUsernameAndHost(
|
||||
{ username: 'al' },
|
||||
{ limit: 100 },
|
||||
);
|
||||
|
@ -240,7 +240,7 @@ describe('UserSearchService', () => {
|
|||
await createFollowings(root, [alice, alyce, alycia, alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]);
|
||||
await setActive([alice, alyce, alycia, alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]);
|
||||
|
||||
const result = await service.search(
|
||||
const result = await service.searchByUsernameAndHost(
|
||||
{ username: 'al', host: 'exam' },
|
||||
{ limit: 100 },
|
||||
root,
|
||||
|
@ -253,7 +253,7 @@ describe('UserSearchService', () => {
|
|||
await setActive([alice, alyce, alycia, alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]);
|
||||
await setSuspended([alice, alyce, alycia]);
|
||||
|
||||
const result = await service.search(
|
||||
const result = await service.searchByUsernameAndHost(
|
||||
{ username: 'al' },
|
||||
{ limit: 100 },
|
||||
root,
|
||||
|
|
|
@ -4,34 +4,37 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<div :class="$style.root">
|
||||
<nav :class="$style.nav">
|
||||
<div :class="$style.navPath" @contextmenu.prevent.stop="() => {}">
|
||||
<XNavFolder
|
||||
:class="[$style.navPathItem, { [$style.navCurrent]: folder == null }]"
|
||||
:parentFolder="folder"
|
||||
@move="move"
|
||||
@upload="upload"
|
||||
@removeFile="removeFile"
|
||||
@removeFolder="removeFolder"
|
||||
/>
|
||||
<template v-for="f in hierarchyFolders">
|
||||
<span :class="[$style.navPathItem, $style.navSeparator]"><i class="ti ti-chevron-right"></i></span>
|
||||
<MkStickyContainer>
|
||||
<template #header>
|
||||
<nav :class="$style.nav">
|
||||
<div :class="$style.navPath" @contextmenu.prevent.stop="() => {}">
|
||||
<XNavFolder
|
||||
:folder="f"
|
||||
:class="[$style.navPathItem, { [$style.navCurrent]: folder == null }]"
|
||||
:parentFolder="folder"
|
||||
:class="[$style.navPathItem]"
|
||||
@move="move"
|
||||
@upload="upload"
|
||||
@removeFile="removeFile"
|
||||
@removeFolder="removeFolder"
|
||||
/>
|
||||
</template>
|
||||
<span v-if="folder != null" :class="[$style.navPathItem, $style.navSeparator]"><i class="ti ti-chevron-right"></i></span>
|
||||
<span v-if="folder != null" :class="[$style.navPathItem, $style.navCurrent]">{{ folder.name }}</span>
|
||||
</div>
|
||||
<button class="_button" :class="$style.navMenu" @click="showMenu"><i class="ti ti-dots"></i></button>
|
||||
</nav>
|
||||
<template v-for="f in hierarchyFolders">
|
||||
<span :class="[$style.navPathItem, $style.navSeparator]"><i class="ti ti-chevron-right"></i></span>
|
||||
<XNavFolder
|
||||
:folder="f"
|
||||
:parentFolder="folder"
|
||||
:class="[$style.navPathItem]"
|
||||
@move="move"
|
||||
@upload="upload"
|
||||
@removeFile="removeFile"
|
||||
@removeFolder="removeFolder"
|
||||
/>
|
||||
</template>
|
||||
<span v-if="folder != null" :class="[$style.navPathItem, $style.navSeparator]"><i class="ti ti-chevron-right"></i></span>
|
||||
<span v-if="folder != null" :class="[$style.navPathItem, $style.navCurrent]">{{ folder.name }}</span>
|
||||
</div>
|
||||
<button class="_button" :class="$style.navMenu" @click="showMenu"><i class="ti ti-dots"></i></button>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<div
|
||||
ref="main"
|
||||
:class="[$style.main, { [$style.uploading]: uploadings.length > 0, [$style.fetching]: fetching }]"
|
||||
|
@ -91,8 +94,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkLoading v-if="fetching"/>
|
||||
</div>
|
||||
<div v-if="draghover" :class="$style.dropzone"></div>
|
||||
<input ref="fileInput" style="display: none;" type="file" accept="*/*" multiple tabindex="-1" @change="onChangeFileInput"/>
|
||||
</div>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
@ -110,6 +112,7 @@ import { i18n } from '@/i18n.js';
|
|||
import { uploadFile, uploads } from '@/utility/upload.js';
|
||||
import { claimAchievement } from '@/utility/achievements.js';
|
||||
import { prefer } from '@/preferences.js';
|
||||
import { chooseFileFromPc } from '@/utility/select-file.js';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
initialFolder?: Misskey.entities.DriveFolder;
|
||||
|
@ -130,7 +133,6 @@ const emit = defineEmits<{
|
|||
}>();
|
||||
|
||||
const loadMoreFiles = useTemplateRef('loadMoreFiles');
|
||||
const fileInput = useTemplateRef('fileInput');
|
||||
|
||||
const folder = ref<Misskey.entities.DriveFolder | null>(null);
|
||||
const files = ref<Misskey.entities.DriveFile[]>([]);
|
||||
|
@ -142,7 +144,6 @@ const selectedFiles = ref<Misskey.entities.DriveFile[]>([]);
|
|||
const selectedFolders = ref<Misskey.entities.DriveFolder[]>([]);
|
||||
const uploadings = uploads;
|
||||
const connection = useStream().useChannel('drive');
|
||||
const keepOriginal = ref<boolean>(prefer.s.keepOriginalUploading); // 外部渡しが多いので$refは使わないほうがよい
|
||||
|
||||
// ドロップされようとしているか
|
||||
const draghover = ref(false);
|
||||
|
@ -304,10 +305,6 @@ function onDrop(ev: DragEvent) {
|
|||
//#endregion
|
||||
}
|
||||
|
||||
function selectLocalFile() {
|
||||
fileInput.value?.click();
|
||||
}
|
||||
|
||||
function urlUpload() {
|
||||
os.inputText({
|
||||
title: i18n.ts.uploadFromUrl,
|
||||
|
@ -383,15 +380,8 @@ function deleteFolder(folderToDelete: Misskey.entities.DriveFolder) {
|
|||
});
|
||||
}
|
||||
|
||||
function onChangeFileInput() {
|
||||
if (!fileInput.value?.files) return;
|
||||
for (const file of Array.from(fileInput.value.files)) {
|
||||
upload(file, folder.value);
|
||||
}
|
||||
}
|
||||
|
||||
function upload(file: File, folderToUpload?: Misskey.entities.DriveFolder | null) {
|
||||
uploadFile(file, (folderToUpload && typeof folderToUpload === 'object') ? folderToUpload.id : null, undefined, keepOriginal.value).then(res => {
|
||||
function upload(file: File, folderToUpload?: Misskey.entities.DriveFolder | null, keepOriginal?: boolean) {
|
||||
uploadFile(file, (folderToUpload && typeof folderToUpload === 'object') ? folderToUpload.id : null, undefined, keepOriginal).then(res => {
|
||||
addFile(res, true);
|
||||
});
|
||||
}
|
||||
|
@ -630,16 +620,20 @@ function getMenu() {
|
|||
const menu: MenuItem[] = [];
|
||||
|
||||
menu.push({
|
||||
type: 'switch',
|
||||
text: i18n.ts.keepOriginalUploading,
|
||||
ref: keepOriginal,
|
||||
}, { type: 'divider' }, {
|
||||
text: i18n.ts.addFile,
|
||||
type: 'label',
|
||||
}, {
|
||||
text: i18n.ts.upload + ' (' + i18n.ts.compress + ')',
|
||||
icon: 'ti ti-upload',
|
||||
action: () => {
|
||||
chooseFileFromPc(true, { keepOriginal: false });
|
||||
},
|
||||
}, {
|
||||
text: i18n.ts.upload,
|
||||
icon: 'ti ti-upload',
|
||||
action: () => { selectLocalFile(); },
|
||||
action: () => {
|
||||
chooseFileFromPc(true, { keepOriginal: true });
|
||||
},
|
||||
}, {
|
||||
text: i18n.ts.fromUrl,
|
||||
icon: 'ti ti-link',
|
||||
|
@ -751,22 +745,17 @@ onBeforeUnmount(() => {
|
|||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.nav {
|
||||
display: flex;
|
||||
z-index: 2;
|
||||
width: 100%;
|
||||
padding: 0 8px;
|
||||
box-sizing: border-box;
|
||||
overflow: auto;
|
||||
font-size: 0.9em;
|
||||
box-shadow: 0 1px 0 var(--MI_THEME-divider);
|
||||
user-select: none;
|
||||
background: color(from var(--MI_THEME-bg) srgb r g b / 0.75);
|
||||
-webkit-backdrop-filter: var(--MI-blur, blur(15px));
|
||||
backdrop-filter: var(--MI-blur, blur(15px));
|
||||
border-bottom: solid 0.5px var(--MI_THEME-divider);
|
||||
}
|
||||
|
||||
.navPath {
|
||||
|
|
|
@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<div v-if="show" ref="el" :class="[$style.root]" :style="{ background: bg }">
|
||||
<div v-if="show" ref="el" :class="[$style.root]">
|
||||
<div :class="[$style.upper, { [$style.slim]: narrow, [$style.thin]: thin_ }]">
|
||||
<div v-if="!thin_ && narrow && props.displayMyAvatar && $i" class="_button" :class="$style.buttonsLeft" @click="openAccountMenu">
|
||||
<MkAvatar :class="$style.avatar" :user="$i"/>
|
||||
|
@ -42,7 +42,6 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, onUnmounted, ref, inject, useTemplateRef, computed } from 'vue';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import { scrollToTop } from '@@/js/scroll.js';
|
||||
import XTabs from './MkPageHeader.tabs.vue';
|
||||
import type { Tab } from './MkPageHeader.tabs.vue';
|
||||
|
@ -78,7 +77,6 @@ const hideTitle = computed(() => inject('shouldOmitHeaderTitle', false) || props
|
|||
const thin_ = props.thin || inject('shouldHeaderThin', false);
|
||||
|
||||
const el = useTemplateRef('el');
|
||||
const bg = ref<string | undefined>(undefined);
|
||||
const narrow = ref(false);
|
||||
const hasTabs = computed(() => props.tabs.length > 0);
|
||||
const hasActions = computed(() => props.actions && props.actions.length > 0);
|
||||
|
@ -106,19 +104,9 @@ function onTabClick(): void {
|
|||
top();
|
||||
}
|
||||
|
||||
const calcBg = () => {
|
||||
const rawBg = 'var(--MI_THEME-bg)';
|
||||
const tinyBg = tinycolor(rawBg.startsWith('var(') ? getComputedStyle(window.document.documentElement).getPropertyValue(rawBg.slice(4, -1)) : rawBg);
|
||||
tinyBg.setAlpha(0.85);
|
||||
bg.value = tinyBg.toRgbString();
|
||||
};
|
||||
|
||||
let ro: ResizeObserver | null;
|
||||
|
||||
onMounted(() => {
|
||||
calcBg();
|
||||
globalEvents.on('themeChanging', calcBg);
|
||||
|
||||
if (el.value && el.value.parentElement) {
|
||||
narrow.value = el.value.parentElement.offsetWidth < 500;
|
||||
ro = new ResizeObserver((entries, observer) => {
|
||||
|
@ -131,13 +119,13 @@ onMounted(() => {
|
|||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
globalEvents.off('themeChanging', calcBg);
|
||||
if (ro) ro.disconnect();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
background: color(from var(--MI_THEME-bg) srgb r g b / 0.75);
|
||||
-webkit-backdrop-filter: var(--MI-blur, blur(15px));
|
||||
backdrop-filter: var(--MI-blur, blur(15px));
|
||||
border-bottom: solid 0.5px var(--MI_THEME-divider);
|
||||
|
|
|
@ -18,11 +18,6 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</option>
|
||||
</MkSelect>
|
||||
|
||||
<MkSwitch v-model="keepOriginalUploading">
|
||||
<template #label>{{ i18n.ts.keepOriginalUploading }}</template>
|
||||
<template #caption>{{ i18n.ts.keepOriginalUploadingDescription }}</template>
|
||||
</MkSwitch>
|
||||
|
||||
<MkSwitch v-model="directoryToCategory">
|
||||
<template #label>{{ i18n.ts._customEmojisManager._local._register.directoryToCategoryLabel }}</template>
|
||||
<template #caption>{{ i18n.ts._customEmojisManager._local._register.directoryToCategoryCaption }}</template>
|
||||
|
@ -245,7 +240,6 @@ function setupGrid(): GridSetting {
|
|||
const uploadFolders = ref<FolderItem[]>([]);
|
||||
const gridItems = ref<GridItem[]>([]);
|
||||
const selectedFolderId = ref(prefer.s.uploadFolder);
|
||||
const keepOriginalUploading = ref(prefer.s.keepOriginalUploading);
|
||||
const directoryToCategory = ref<boolean>(false);
|
||||
const registerButtonDisabled = ref<boolean>(false);
|
||||
const requestLogs = ref<RequestLogItem[]>([]);
|
||||
|
@ -338,7 +332,7 @@ async function onDrop(ev: DragEvent) {
|
|||
it.file,
|
||||
selectedFolderId.value,
|
||||
it.file.name.replace(/\.[^.]+$/, ''),
|
||||
keepOriginalUploading.value,
|
||||
true,
|
||||
),
|
||||
}),
|
||||
),
|
||||
|
@ -373,7 +367,7 @@ async function onFileSelectClicked() {
|
|||
true,
|
||||
{
|
||||
uploadFolder: selectedFolderId.value,
|
||||
keepOriginal: keepOriginalUploading.value,
|
||||
keepOriginal: true,
|
||||
// 拡張子は消す
|
||||
nameConverter: (file) => file.name.replace(/\.[a-zA-Z0-9]+$/, ''),
|
||||
},
|
||||
|
|
|
@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<template>
|
||||
<div>
|
||||
<XDrive ref="drive" @cd="x => folder = x"/>
|
||||
<XDrive @cd="x => folder = x"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -53,15 +53,6 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
{{ i18n.ts.drivecleaner }}
|
||||
</FormLink>
|
||||
|
||||
<SearchMarker :keywords="['keep', 'original', 'raw', 'upload']">
|
||||
<MkPreferenceContainer k="keepOriginalUploading">
|
||||
<MkSwitch v-model="keepOriginalUploading">
|
||||
<template #label><SearchLabel>{{ i18n.ts.keepOriginalUploading }}</SearchLabel></template>
|
||||
<template #caption><SearchKeyword>{{ i18n.ts.keepOriginalUploadingDescription }}</SearchKeyword></template>
|
||||
</MkSwitch>
|
||||
</MkPreferenceContainer>
|
||||
</SearchMarker>
|
||||
|
||||
<SearchMarker :keywords="['keep', 'original', 'filename']">
|
||||
<MkPreferenceContainer k="keepOriginalFilename">
|
||||
<MkSwitch v-model="keepOriginalFilename">
|
||||
|
@ -130,7 +121,6 @@ const meterStyle = computed(() => {
|
|||
};
|
||||
});
|
||||
|
||||
const keepOriginalUploading = prefer.model('keepOriginalUploading');
|
||||
const keepOriginalFilename = prefer.model('keepOriginalFilename');
|
||||
|
||||
misskeyApi('drive').then(info => {
|
||||
|
|
|
@ -66,7 +66,6 @@ export function migrateOldSettings() {
|
|||
prefer.commit('collapseRenotes', store.s.collapseRenotes);
|
||||
prefer.commit('rememberNoteVisibility', store.s.rememberNoteVisibility);
|
||||
prefer.commit('uploadFolder', store.s.uploadFolder);
|
||||
prefer.commit('keepOriginalUploading', store.s.keepOriginalUploading);
|
||||
prefer.commit('menu', store.s.menu);
|
||||
prefer.commit('statusbars', store.s.statusbars);
|
||||
prefer.commit('pinnedUserLists', store.s.pinnedUserLists);
|
||||
|
|
|
@ -118,9 +118,6 @@ export const PREF_DEF = {
|
|||
keepCw: {
|
||||
default: true,
|
||||
},
|
||||
keepOriginalUploading: {
|
||||
default: false,
|
||||
},
|
||||
rememberNoteVisibility: {
|
||||
default: false,
|
||||
},
|
||||
|
|
|
@ -16,8 +16,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
|
||||
<main class="main" @contextmenu.stop="onContextmenu">
|
||||
<div class="content" style="container-type: inline-size;">
|
||||
<RouterView/>
|
||||
<div class="content">
|
||||
<StackingRouterView v-if="prefer.s['experimental.stackingRouterView']"/>
|
||||
<RouterView v-else/>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
|
@ -318,4 +319,8 @@ onMounted(() => {
|
|||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -759,21 +759,16 @@ export const searchIndexes: SearchIndexItem[] = [
|
|||
},
|
||||
{
|
||||
id: 'goQdtf3dD',
|
||||
label: i18n.ts.keepOriginalUploading,
|
||||
keywords: ['keep', 'original', 'raw', 'upload', i18n.ts.keepOriginalUploadingDescription],
|
||||
},
|
||||
{
|
||||
id: '83xRo0XJl',
|
||||
label: i18n.ts.keepOriginalFilename,
|
||||
keywords: ['keep', 'original', 'filename', i18n.ts.keepOriginalFilenameDescription],
|
||||
},
|
||||
{
|
||||
id: 'wf77yRQQq',
|
||||
id: '83xRo0XJl',
|
||||
label: i18n.ts.alwaysMarkSensitive,
|
||||
keywords: ['always', 'default', 'mark', 'nsfw', 'sensitive', 'media', 'file'],
|
||||
},
|
||||
{
|
||||
id: '3pxwNB8e4',
|
||||
id: 'BrBqZL35E',
|
||||
label: i18n.ts.enableAutoSensitive,
|
||||
keywords: ['auto', 'nsfw', 'sensitive', 'media', 'file', i18n.ts.enableAutoSensitiveDescription],
|
||||
},
|
||||
|
|
|
@ -21,7 +21,7 @@ export function chooseFileFromPc(
|
|||
},
|
||||
): Promise<Misskey.entities.DriveFile[]> {
|
||||
const uploadFolder = options?.uploadFolder ?? prefer.s.uploadFolder;
|
||||
const keepOriginal = options?.keepOriginal ?? prefer.s.keepOriginalUploading;
|
||||
const keepOriginal = options?.keepOriginal ?? false;
|
||||
const nameConverter = options?.nameConverter ?? (() => undefined);
|
||||
|
||||
return new Promise((res, rej) => {
|
||||
|
@ -96,19 +96,17 @@ export function chooseFileFromUrl(): Promise<Misskey.entities.DriveFile> {
|
|||
|
||||
function select(src: HTMLElement | EventTarget | null, label: string | null, multiple: boolean): Promise<Misskey.entities.DriveFile[]> {
|
||||
return new Promise((res, rej) => {
|
||||
const keepOriginal = ref(prefer.s.keepOriginalUploading);
|
||||
|
||||
os.popupMenu([label ? {
|
||||
text: label,
|
||||
type: 'label',
|
||||
} : undefined, {
|
||||
type: 'switch',
|
||||
text: i18n.ts.keepOriginalUploading,
|
||||
ref: keepOriginal,
|
||||
text: i18n.ts.upload + ' (' + i18n.ts.compress + ')',
|
||||
icon: 'ti ti-upload',
|
||||
action: () => chooseFileFromPc(multiple, { keepOriginal: false }).then(files => res(files)),
|
||||
}, {
|
||||
text: i18n.ts.upload,
|
||||
icon: 'ti ti-upload',
|
||||
action: () => chooseFileFromPc(multiple, { keepOriginal: keepOriginal.value }).then(files => res(files)),
|
||||
action: () => chooseFileFromPc(multiple, { keepOriginal: true }).then(files => res(files)),
|
||||
}, {
|
||||
text: i18n.ts.fromDrive,
|
||||
icon: 'ti ti-cloud',
|
||||
|
|
|
@ -34,7 +34,7 @@ export function uploadFile(
|
|||
file: File,
|
||||
folder?: string | Misskey.entities.DriveFolder | null,
|
||||
name?: string,
|
||||
keepOriginal: boolean = prefer.s.keepOriginalUploading,
|
||||
keepOriginal = false,
|
||||
): Promise<Misskey.entities.DriveFile> {
|
||||
if ($i == null) throw new Error('Not logged in');
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"type": "module",
|
||||
"name": "misskey-js",
|
||||
"version": "2025.3.2-beta.13",
|
||||
"version": "2025.3.2-beta.14",
|
||||
"description": "Misskey SDK for JavaScript",
|
||||
"license": "MIT",
|
||||
"main": "./built/index.js",
|
||||
|
|
Loading…
Reference in New Issue