Compare commits
48 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
e0fc8cbf8f | ||
![]() |
f9d1bc340e | ||
![]() |
0b269e79fd | ||
![]() |
6159cfd138 | ||
![]() |
6a5bbd335b | ||
![]() |
39e269db8c | ||
![]() |
70fe23a3ce | ||
![]() |
a6a8a7fb85 | ||
![]() |
6641b13b4c | ||
![]() |
5136b05c9b | ||
![]() |
803c2144f4 | ||
![]() |
b69a079514 | ||
![]() |
2aa800cd55 | ||
![]() |
6e61a36d05 | ||
![]() |
f80bf1fb1c | ||
![]() |
d465e85239 | ||
![]() |
deed25a2ff | ||
![]() |
a486716520 | ||
![]() |
2361e11e98 | ||
![]() |
cd1f2adca7 | ||
![]() |
a558767b7a | ||
![]() |
399ce9b999 | ||
![]() |
a94a0b5b0b | ||
![]() |
76faec2115 | ||
![]() |
33c4e57994 | ||
![]() |
bc23496998 | ||
![]() |
d35ad95c18 | ||
![]() |
5facd11592 | ||
![]() |
e1e885d6b2 | ||
![]() |
5b6695114f | ||
![]() |
71dd7f89e9 | ||
![]() |
21331e53fe | ||
![]() |
7afee5977f | ||
![]() |
d195b0dec7 | ||
![]() |
8a95e850ad | ||
![]() |
a4d74d7d7e | ||
![]() |
256e0db36d | ||
![]() |
d593c1358a | ||
![]() |
1ff14d81c1 | ||
![]() |
4369d12eec | ||
![]() |
91cc033eb5 | ||
![]() |
57543e6b44 | ||
![]() |
a1b8cd15c4 | ||
![]() |
73f06e591a | ||
![]() |
6f7cfa82b5 | ||
![]() |
ff97a003d1 | ||
![]() |
53c92e3e23 | ||
![]() |
13d13bc2f6 |
24
CHANGELOG.md
24
CHANGELOG.md
@@ -8,6 +8,30 @@
|
||||
|
||||
You should also include the user name that made the change.
|
||||
-->
|
||||
## 13.5.4 (2023/02/09)
|
||||
|
||||
### Improvements
|
||||
- Server: UIのHTML(ノートなどの特別なページを除く)のキャッシュ時間を15秒から30秒に
|
||||
- i/notificationsのレートリミットを緩和
|
||||
|
||||
### Bugfixes
|
||||
- fix(client): validate url to improve security
|
||||
- fix(client): dateの初期値が正常に入らない時がある
|
||||
|
||||
## 13.5.3 (2023/02/09)
|
||||
|
||||
### Improvements
|
||||
- Client: デッキにチャンネルカラムを追加
|
||||
|
||||
## 13.5.2 (2023/02/08)
|
||||
|
||||
### Changes
|
||||
- Revert: perf(client): do not render custom emojis in user names
|
||||
|
||||
### Bugfixes
|
||||
- Client: register_note_view_interruptor not working
|
||||
- Client: ログイントークンの再生成が出来ない
|
||||
|
||||
## 13.5.0 (2023/02/08)
|
||||
|
||||
### Changes
|
||||
|
@@ -6,16 +6,13 @@ Also, the later tasks are more indefinite and are subject to change as developme
|
||||
This is the phase we are at now. We need to make a high-maintenance environment that can withstand future development.
|
||||
|
||||
- Make the number of type errors zero (backend)
|
||||
- Probably need to switch some libraries to others that make it difficult to reduce type errors
|
||||
- e.g. koa to fastify https://github.com/misskey-dev/misskey/issues/7537
|
||||
- Improve CI
|
||||
- Fix tests
|
||||
- mocha, jest, etc. do not support the combination of `TypeScript + ESM + Path alias`, and the tests currently do not work.
|
||||
- Fix random test failures - https://github.com/misskey-dev/misskey/issues/7985 and https://github.com/misskey-dev/misskey/issues/7986
|
||||
- Add more tests
|
||||
- May need to implement a mechanism that allows for DI
|
||||
- ~~May need to implement a mechanism that allows for DI~~ → Done ✔️
|
||||
- https://github.com/misskey-dev/misskey/pull/9085
|
||||
- Measure coverage
|
||||
- ~~Measure coverage~~ → Done ✔️
|
||||
- https://github.com/misskey-dev/misskey/pull/9081
|
||||
- Improve documentation
|
||||
- Refactoring
|
||||
|
@@ -1345,5 +1345,6 @@ _deck:
|
||||
tl: "الخيط الزمني"
|
||||
antenna: "الهوائيات"
|
||||
list: "القوائم"
|
||||
channel: "القنوات"
|
||||
mentions: "الإشارات"
|
||||
direct: "مباشرة"
|
||||
|
@@ -1441,5 +1441,6 @@ _deck:
|
||||
tl: "টাইমলাইন"
|
||||
antenna: "অ্যান্টেনা"
|
||||
list: "লিস্ট"
|
||||
channel: "চ্যানেলগুলি"
|
||||
mentions: "উল্লেখসমূহ"
|
||||
direct: "ডাইরেক্ট নোটগুলি"
|
||||
|
@@ -804,4 +804,5 @@ _deck:
|
||||
tl: "Časová osa"
|
||||
antenna: "Antény"
|
||||
list: "Seznamy"
|
||||
channel: "Kanály"
|
||||
mentions: "Zmínění"
|
||||
|
@@ -1869,5 +1869,6 @@ _deck:
|
||||
tl: "Chronik"
|
||||
antenna: "Antennen"
|
||||
list: "Listen"
|
||||
channel: "Kanäle"
|
||||
mentions: "Erwähnungen"
|
||||
direct: "Direktnachrichten"
|
||||
|
@@ -1869,5 +1869,6 @@ _deck:
|
||||
tl: "Timeline"
|
||||
antenna: "Antennas"
|
||||
list: "List"
|
||||
channel: "Channels"
|
||||
mentions: "Mentions"
|
||||
direct: "Direct notes"
|
||||
|
@@ -129,6 +129,7 @@ unblockConfirm: "¿Quiere dejar de bloquear esta cuenta?"
|
||||
suspendConfirm: "¿Quiere suspender esta cuenta?"
|
||||
unsuspendConfirm: "¿Quiere dejar de suspender esta cuenta?"
|
||||
selectList: "Seleccione una lista"
|
||||
selectChannel: "Seleccionar canal"
|
||||
selectAntenna: "Seleccionar antena"
|
||||
selectWidget: "Seleccionar widget"
|
||||
editWidgets: "Editar widgets"
|
||||
@@ -1869,5 +1870,6 @@ _deck:
|
||||
tl: "Linea de tiempo"
|
||||
antenna: "Antenas"
|
||||
list: "Listas"
|
||||
channel: "Canal"
|
||||
mentions: "Menciones"
|
||||
direct: "Mensaje directo"
|
||||
|
@@ -1541,5 +1541,6 @@ _deck:
|
||||
tl: "Fil"
|
||||
antenna: "Antennes"
|
||||
list: "Listes"
|
||||
channel: "Canaux"
|
||||
mentions: "Mentions"
|
||||
direct: "Direct"
|
||||
|
@@ -1673,5 +1673,6 @@ _deck:
|
||||
tl: "Linimasa"
|
||||
antenna: "Antena"
|
||||
list: "Daftar"
|
||||
channel: "Kanal"
|
||||
mentions: "Sebutan"
|
||||
direct: "Langsung"
|
||||
|
@@ -1869,5 +1869,6 @@ _deck:
|
||||
tl: "Timeline"
|
||||
antenna: "Antenne"
|
||||
list: "Liste"
|
||||
channel: "Canale"
|
||||
mentions: "Menzioni"
|
||||
direct: "Diretta"
|
||||
|
@@ -129,6 +129,7 @@ unblockConfirm: "ブロック解除しますか?"
|
||||
suspendConfirm: "凍結しますか?"
|
||||
unsuspendConfirm: "解凍しますか?"
|
||||
selectList: "リストを選択"
|
||||
selectChannel: "チャンネルを選択"
|
||||
selectAntenna: "アンテナを選択"
|
||||
selectWidget: "ウィジェットを選択"
|
||||
editWidgets: "ウィジェットを編集"
|
||||
@@ -939,6 +940,8 @@ cannotPerformTemporaryDescription: "操作回数が制限を超過するため
|
||||
preset: "プリセット"
|
||||
selectFromPresets: "プリセットから選択"
|
||||
achievements: "実績"
|
||||
gotInvalidResponseError: "サーバーの応答が無効です"
|
||||
gotInvalidResponseErrorDescription: "サーバーがダウンまたはメンテナンスしている可能性があります。しばらくしてから再度お試しください。"
|
||||
|
||||
_achievements:
|
||||
earnedAt: "獲得日時"
|
||||
@@ -1922,5 +1925,6 @@ _deck:
|
||||
tl: "タイムライン"
|
||||
antenna: "アンテナ"
|
||||
list: "リスト"
|
||||
channel: "チャンネル"
|
||||
mentions: "あなた宛て"
|
||||
direct: "ダイレクト"
|
||||
|
@@ -1628,5 +1628,6 @@ _deck:
|
||||
tl: "タイムライン"
|
||||
antenna: "アンテナ"
|
||||
list: "リスト"
|
||||
channel: "チャンネル"
|
||||
mentions: "あんた宛て"
|
||||
direct: "ダイレクト"
|
||||
|
@@ -1866,5 +1866,6 @@ _deck:
|
||||
tl: "타임라인"
|
||||
antenna: "안테나"
|
||||
list: "리스트"
|
||||
channel: "채널"
|
||||
mentions: "받은 멘션"
|
||||
direct: "다이렉트"
|
||||
|
@@ -1438,5 +1438,6 @@ _deck:
|
||||
tl: "Oś czasu"
|
||||
antenna: "Anteny"
|
||||
list: "Listy"
|
||||
channel: "Kanały"
|
||||
mentions: "Wspomnienia"
|
||||
direct: "Bezpośredni"
|
||||
|
@@ -721,4 +721,5 @@ _deck:
|
||||
tl: "Cronologie"
|
||||
antenna: "Antene"
|
||||
list: "Liste"
|
||||
channel: "Canale"
|
||||
mentions: "Mențiuni"
|
||||
|
@@ -1845,5 +1845,6 @@ _deck:
|
||||
tl: "Лента"
|
||||
antenna: "Антенны"
|
||||
list: "Списки"
|
||||
channel: "Каналы"
|
||||
mentions: "Упоминания"
|
||||
direct: "Личное"
|
||||
|
@@ -1545,5 +1545,6 @@ _deck:
|
||||
tl: "Časová os"
|
||||
antenna: "Antény"
|
||||
list: "Zoznam"
|
||||
channel: "Kanály"
|
||||
mentions: "Zmienky"
|
||||
direct: "Priame poznámky"
|
||||
|
@@ -1869,5 +1869,6 @@ _deck:
|
||||
tl: "ไทม์ไลน์"
|
||||
antenna: "เสาอากาศ"
|
||||
list: "รายการ"
|
||||
channel: "แชนแนล"
|
||||
mentions: "พูดถึง"
|
||||
direct: "ไดเร็ค"
|
||||
|
@@ -1689,5 +1689,6 @@ _deck:
|
||||
tl: "Стрічка"
|
||||
antenna: "Антени"
|
||||
list: "Списки"
|
||||
channel: "Канали"
|
||||
mentions: "Згадки"
|
||||
direct: "Особисте"
|
||||
|
@@ -1520,5 +1520,6 @@ _deck:
|
||||
tl: "Bảng tin"
|
||||
antenna: "Trạm phát sóng"
|
||||
list: "Danh sách"
|
||||
channel: "Kênh"
|
||||
mentions: "Lượt nhắc"
|
||||
direct: "Nhắn riêng"
|
||||
|
@@ -1869,5 +1869,6 @@ _deck:
|
||||
tl: "时间线"
|
||||
antenna: "天线"
|
||||
list: "列表"
|
||||
channel: "频道"
|
||||
mentions: "提及"
|
||||
direct: "指定用户"
|
||||
|
@@ -1869,5 +1869,6 @@ _deck:
|
||||
tl: "時間軸"
|
||||
antenna: "天線"
|
||||
list: "清單"
|
||||
channel: "頻道"
|
||||
mentions: "提及"
|
||||
direct: "指定使用者"
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "misskey",
|
||||
"version": "13.5.0",
|
||||
"version": "13.5.4",
|
||||
"codename": "nasubi",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -54,8 +54,8 @@
|
||||
"devDependencies": {
|
||||
"@types/gulp": "4.0.10",
|
||||
"@types/gulp-rename": "2.0.1",
|
||||
"@typescript-eslint/eslint-plugin": "5.50.0",
|
||||
"@typescript-eslint/parser": "5.50.0",
|
||||
"@typescript-eslint/eslint-plugin": "5.51.0",
|
||||
"@typescript-eslint/parser": "5.51.0",
|
||||
"cross-env": "7.0.3",
|
||||
"cypress": "12.5.1",
|
||||
"eslint": "8.33.0",
|
||||
|
@@ -23,9 +23,9 @@
|
||||
"@tensorflow/tfjs-node": "4.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@bull-board/api": "4.11.0",
|
||||
"@bull-board/fastify": "4.11.0",
|
||||
"@bull-board/ui": "4.11.0",
|
||||
"@bull-board/api": "4.11.1",
|
||||
"@bull-board/fastify": "4.11.1",
|
||||
"@bull-board/ui": "4.11.1",
|
||||
"@discordapp/twemoji": "14.0.2",
|
||||
"@fastify/accepts": "4.1.0",
|
||||
"@fastify/cookie": "8.3.0",
|
||||
@@ -34,9 +34,9 @@
|
||||
"@fastify/multipart": "7.4.0",
|
||||
"@fastify/static": "6.8.0",
|
||||
"@fastify/view": "7.4.1",
|
||||
"@nestjs/common": "9.3.1",
|
||||
"@nestjs/core": "9.3.1",
|
||||
"@nestjs/testing": "9.3.1",
|
||||
"@nestjs/common": "9.3.7",
|
||||
"@nestjs/core": "9.3.7",
|
||||
"@nestjs/testing": "9.3.7",
|
||||
"@peertube/http-signature": "1.7.0",
|
||||
"@sinonjs/fake-timers": "10.0.2",
|
||||
"accepts": "1.3.8",
|
||||
@@ -46,7 +46,7 @@
|
||||
"aws-sdk": "2.1295.0",
|
||||
"bcryptjs": "2.4.3",
|
||||
"blurhash": "2.0.4",
|
||||
"bull": "4.10.2",
|
||||
"bull": "4.10.3",
|
||||
"cacheable-lookup": "6.1.0",
|
||||
"cbor": "8.1.0",
|
||||
"chalk": "5.2.0",
|
||||
@@ -90,7 +90,7 @@
|
||||
"promise-limit": "2.7.0",
|
||||
"pug": "3.0.2",
|
||||
"punycode": "2.3.0",
|
||||
"pureimage": "0.3.15",
|
||||
"pureimage": "0.3.17",
|
||||
"qrcode": "1.5.1",
|
||||
"random-seed": "0.3.0",
|
||||
"ratelimiter": "3.4.1",
|
||||
@@ -111,12 +111,12 @@
|
||||
"stringz": "2.1.0",
|
||||
"summaly": "2.7.0",
|
||||
"systeminformation": "5.17.8",
|
||||
"tinycolor2": "1.5.2",
|
||||
"tinycolor2": "1.6.0",
|
||||
"tmp": "0.2.1",
|
||||
"tsc-alias": "1.8.2",
|
||||
"tsconfig-paths": "4.1.2",
|
||||
"twemoji-parser": "14.0.0",
|
||||
"typeorm": "0.3.11",
|
||||
"typeorm": "0.3.12",
|
||||
"typescript": "4.9.5",
|
||||
"ulid": "2.3.0",
|
||||
"unzipper": "0.10.11",
|
||||
@@ -128,10 +128,10 @@
|
||||
"xev": "3.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@jest/globals": "29.4.1",
|
||||
"@jest/globals": "29.4.2",
|
||||
"@redocly/openapi-core": "1.0.0-beta.123",
|
||||
"@swc/cli": "0.1.61",
|
||||
"@swc/core": "1.3.32",
|
||||
"@swc/core": "1.3.34",
|
||||
"@swc/jest": "0.2.24",
|
||||
"@types/accepts": "1.3.5",
|
||||
"@types/archiver": "5.3.1",
|
||||
@@ -145,11 +145,11 @@
|
||||
"@types/ioredis": "4.28.10",
|
||||
"@types/jest": "29.4.0",
|
||||
"@types/js-yaml": "4.0.5",
|
||||
"@types/jsdom": "20.0.1",
|
||||
"@types/jsdom": "21.1.0",
|
||||
"@types/jsonld": "1.5.8",
|
||||
"@types/jsrsasign": "10.5.5",
|
||||
"@types/mime-types": "2.1.1",
|
||||
"@types/node": "18.11.18",
|
||||
"@types/node": "18.13.0",
|
||||
"@types/node-fetch": "3.0.3",
|
||||
"@types/nodemailer": "6.4.7",
|
||||
"@types/oauth": "0.9.1",
|
||||
@@ -174,13 +174,13 @@
|
||||
"@types/web-push": "3.3.2",
|
||||
"@types/websocket": "1.0.5",
|
||||
"@types/ws": "8.5.4",
|
||||
"@typescript-eslint/eslint-plugin": "5.50.0",
|
||||
"@typescript-eslint/parser": "5.50.0",
|
||||
"@typescript-eslint/eslint-plugin": "5.51.0",
|
||||
"@typescript-eslint/parser": "5.51.0",
|
||||
"cross-env": "7.0.3",
|
||||
"eslint": "8.33.0",
|
||||
"eslint-plugin-import": "2.27.5",
|
||||
"execa": "6.1.0",
|
||||
"jest": "29.4.1",
|
||||
"jest-mock": "29.4.1"
|
||||
"jest": "29.4.2",
|
||||
"jest-mock": "29.4.2"
|
||||
}
|
||||
}
|
||||
|
@@ -5,7 +5,7 @@ import { DI } from '@/di-symbols.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { CreateNotificationService } from '@/core/CreateNotificationService.js';
|
||||
|
||||
const ACHIEVEMENT_TYPES = [
|
||||
export const ACHIEVEMENT_TYPES = [
|
||||
'notes1',
|
||||
'notes10',
|
||||
'notes100',
|
||||
|
@@ -150,9 +150,17 @@ export class CustomEmojiService {
|
||||
if (note.renote) {
|
||||
emojis = emojis.concat(note.renote.emojis
|
||||
.map(e => this.parseEmojiStr(e, note.renote!.userHost)));
|
||||
if (note.renote.user) {
|
||||
emojis = emojis.concat(note.renote.user.emojis
|
||||
.map(e => this.parseEmojiStr(e, note.renote!.userHost)));
|
||||
}
|
||||
}
|
||||
const customReactions = Object.keys(note.reactions).map(x => this.reactionService.decodeReaction(x)).filter(x => x.name != null) as typeof emojis;
|
||||
emojis = emojis.concat(customReactions);
|
||||
if (note.user) {
|
||||
emojis = emojis.concat(note.user.emojis
|
||||
.map(e => this.parseEmojiStr(e, note.userHost)));
|
||||
}
|
||||
}
|
||||
return emojis.filter(x => x.name != null && x.host != null) as { name: string; host: string; }[];
|
||||
}
|
||||
|
@@ -1,10 +1,10 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Brackets } from 'typeorm';
|
||||
import { Brackets, ObjectLiteral } from 'typeorm';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { User } from '@/models/entities/User.js';
|
||||
import type { UserProfilesRepository, FollowingsRepository, ChannelFollowingsRepository, MutedNotesRepository, BlockingsRepository, NoteThreadMutingsRepository, MutingsRepository } from '@/models/index.js';
|
||||
import type { SelectQueryBuilder } from 'typeorm';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import type { SelectQueryBuilder } from 'typeorm';
|
||||
|
||||
@Injectable()
|
||||
export class QueryService {
|
||||
@@ -32,7 +32,7 @@ export class QueryService {
|
||||
) {
|
||||
}
|
||||
|
||||
public makePaginationQuery<T>(q: SelectQueryBuilder<T>, sinceId?: string, untilId?: string, sinceDate?: number, untilDate?: number): SelectQueryBuilder<T> {
|
||||
public makePaginationQuery<T extends ObjectLiteral>(q: SelectQueryBuilder<T>, sinceId?: string, untilId?: string, sinceDate?: number, untilDate?: number): SelectQueryBuilder<T> {
|
||||
if (sinceId && untilId) {
|
||||
q.andWhere(`${q.alias}.id > :sinceId`, { sinceId: sinceId });
|
||||
q.andWhere(`${q.alias}.id < :untilId`, { untilId: untilId });
|
||||
|
@@ -413,6 +413,7 @@ export class UserEntityService implements OnModuleInit {
|
||||
faviconUrl: instance.faviconUrl,
|
||||
themeColor: instance.themeColor,
|
||||
} : undefined) : undefined,
|
||||
emojis: this.customEmojiService.populateEmojis(user.emojis, user.host),
|
||||
onlineStatus: this.getOnlineStatus(user),
|
||||
// パフォーマンス上の理由でローカルユーザーのみ
|
||||
badgeRoles: user.host == null ? this.roleService.getUserBadgeRoles(user.id).then(rs => rs.map(r => ({
|
||||
@@ -463,7 +464,6 @@ export class UserEntityService implements OnModuleInit {
|
||||
isModerator: role.isModerator,
|
||||
isAdministrator: role.isAdministrator,
|
||||
}))),
|
||||
emojis: this.customEmojiService.populateEmojis(user.emojis, user.host),
|
||||
} : {}),
|
||||
|
||||
...(opts.detail && isMe ? {
|
||||
|
@@ -5,7 +5,7 @@
|
||||
* The getter will return a .bind version of the function
|
||||
* and memoize the result against a symbol on the instance
|
||||
*/
|
||||
export function bindThis(target, key, descriptor) {
|
||||
export function bindThis(target: any, key: string, descriptor: any) {
|
||||
let fn = descriptor.value;
|
||||
|
||||
if (typeof fn !== 'function') {
|
||||
@@ -34,7 +34,7 @@ export function bindThis(target, key, descriptor) {
|
||||
});
|
||||
return boundFn;
|
||||
},
|
||||
set(value) {
|
||||
set(value: any) {
|
||||
fn = value;
|
||||
},
|
||||
};
|
||||
|
@@ -45,7 +45,7 @@ export default class Logger {
|
||||
}
|
||||
|
||||
const time = dateFormat(new Date(), 'HH:mm:ss');
|
||||
const worker = cluster.isPrimary ? '*' : cluster.worker.id;
|
||||
const worker = cluster.isPrimary ? '*' : cluster.worker!.id;
|
||||
const l =
|
||||
level === 'error' ? important ? chalk.bgRed.white('ERR ') : chalk.red('ERR ') :
|
||||
level === 'warning' ? chalk.yellow('WARN') :
|
||||
|
@@ -51,7 +51,7 @@ export function genIdenticon(seed: string, stream: WriteStream): Promise<void> {
|
||||
bg.addColorStop(0, bgColors[0]);
|
||||
bg.addColorStop(1, bgColors[1]);
|
||||
|
||||
ctx.fillStyle = bg;
|
||||
ctx.fillStyle = bg as any;
|
||||
ctx.beginPath();
|
||||
ctx.fillRect(0, 0, size, size);
|
||||
|
||||
|
@@ -11,10 +11,9 @@ export class I18n<T extends Record<string, any>> {
|
||||
|
||||
// string にしているのは、ドット区切りでのパス指定を許可するため
|
||||
// なるべくこのメソッド使うよりもlocale直接参照の方がvueのキャッシュ効いてパフォーマンスが良いかも
|
||||
@bindThis
|
||||
public t(key: string, args?: Record<string, any>): string {
|
||||
try {
|
||||
let str = key.split('.').reduce((o, i) => o[i], this.locale) as string;
|
||||
let str = key.split('.').reduce((o, i) => o[i], this.locale as any) as string;
|
||||
|
||||
if (args) {
|
||||
for (const [k, v] of Object.entries(args)) {
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import { IncomingMessage } from 'node:http';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import fastifyAccepts from '@fastify/accepts';
|
||||
import httpSignature from '@peertube/http-signature';
|
||||
@@ -19,6 +20,7 @@ import { QueryService } from '@/core/QueryService.js';
|
||||
import { UtilityService } from '@/core/UtilityService.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { IActivity } from '@/core/activitypub/type.js';
|
||||
import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions } from 'fastify';
|
||||
import type { FindOptionsWhere } from 'typeorm';
|
||||
|
||||
@@ -97,7 +99,8 @@ export class ActivityPubServerService {
|
||||
return;
|
||||
}
|
||||
|
||||
this.queueService.inbox(request.body, signature);
|
||||
// TODO: request.bodyのバリデーション?
|
||||
this.queueService.inbox(request.body as IActivity, signature);
|
||||
|
||||
reply.code(202);
|
||||
}
|
||||
@@ -413,20 +416,21 @@ export class ActivityPubServerService {
|
||||
|
||||
@bindThis
|
||||
public createServer(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) {
|
||||
fastify.addConstraintStrategy({
|
||||
// addConstraintStrategy の型定義がおかしいため
|
||||
(fastify.addConstraintStrategy as any)({
|
||||
name: 'apOrHtml',
|
||||
storage() {
|
||||
const store = {};
|
||||
const store = {} as any;
|
||||
return {
|
||||
get(key) {
|
||||
get(key: string) {
|
||||
return store[key] ?? null;
|
||||
},
|
||||
set(key, value) {
|
||||
set(key: string, value: any) {
|
||||
store[key] = value;
|
||||
},
|
||||
};
|
||||
},
|
||||
deriveConstraint(request, ctx) {
|
||||
deriveConstraint(request: IncomingMessage) {
|
||||
const accepted = accepts(request).type(['html', ACTIVITY_JSON, LD_JSON]);
|
||||
const isAp = typeof accepted === 'string' && !accepted.match(/html/);
|
||||
return isAp ? 'ap' : 'html';
|
||||
@@ -536,6 +540,7 @@ export class ActivityPubServerService {
|
||||
return (this.apRendererService.renderActivity(this.apRendererService.renderKey(user, keypair)));
|
||||
} else {
|
||||
reply.code(400);
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
|
@@ -166,6 +166,7 @@ export class ServerService {
|
||||
return 'Verify succeeded!';
|
||||
} else {
|
||||
reply.code(404);
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
|
@@ -34,7 +34,7 @@ export class RateLimiterService {
|
||||
const min = (): void => {
|
||||
const minIntervalLimiter = new Limiter({
|
||||
id: `${actor}:${limitation.key}:min`,
|
||||
duration: limitation.minInterval * factor,
|
||||
duration: limitation.minInterval! * factor,
|
||||
max: 1,
|
||||
db: this.redisClient,
|
||||
});
|
||||
@@ -62,8 +62,8 @@ export class RateLimiterService {
|
||||
const max = (): void => {
|
||||
const limiter = new Limiter({
|
||||
id: `${actor}:${limitation.key}`,
|
||||
duration: limitation.duration * factor,
|
||||
max: limitation.max / factor,
|
||||
duration: limitation.duration! * factor,
|
||||
max: limitation.max! / factor,
|
||||
db: this.redisClient,
|
||||
});
|
||||
|
||||
|
@@ -10,9 +10,9 @@ import { getIpHash } from '@/misc/get-ip-hash.js';
|
||||
import type { ILocalUser } from '@/models/entities/User.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { TwoFactorAuthenticationService } from '@/core/TwoFactorAuthenticationService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { RateLimiterService } from './RateLimiterService.js';
|
||||
import { SigninService } from './SigninService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import type { FastifyRequest, FastifyReply } from 'fastify';
|
||||
|
||||
@Injectable()
|
||||
@@ -131,7 +131,7 @@ export class SigninApiService {
|
||||
createdAt: new Date(),
|
||||
userId: user.id,
|
||||
ip: request.ip,
|
||||
headers: request.headers,
|
||||
headers: request.headers as any,
|
||||
success: false,
|
||||
});
|
||||
|
||||
|
@@ -25,7 +25,7 @@ export class SigninService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public signin(request: FastifyRequest, reply: FastifyReply, user: ILocalUser, redirect = false) {
|
||||
public signin(request: FastifyRequest, reply: FastifyReply, user: ILocalUser) {
|
||||
setImmediate(async () => {
|
||||
// Append signin history
|
||||
const record = await this.signinsRepository.insert({
|
||||
@@ -33,7 +33,7 @@ export class SigninService {
|
||||
createdAt: new Date(),
|
||||
userId: user.id,
|
||||
ip: request.ip,
|
||||
headers: request.headers,
|
||||
headers: request.headers as any,
|
||||
success: true,
|
||||
}).then(x => this.signinsRepository.findOneByOrFail(x.identifiers[0]));
|
||||
|
||||
@@ -41,25 +41,11 @@ export class SigninService {
|
||||
this.globalEventService.publishMainStream(user.id, 'signin', await this.signinEntityService.pack(record));
|
||||
});
|
||||
|
||||
if (redirect) {
|
||||
//#region Cookie
|
||||
reply.setCookie('igi', user.token!, {
|
||||
path: '/',
|
||||
// SEE: https://github.com/koajs/koa/issues/974
|
||||
// When using a SSL proxy it should be configured to add the "X-Forwarded-Proto: https" header
|
||||
secure: this.config.url.startsWith('https'),
|
||||
httpOnly: false,
|
||||
});
|
||||
//#endregion
|
||||
|
||||
reply.redirect(this.config.url);
|
||||
} else {
|
||||
reply.code(200);
|
||||
return {
|
||||
id: user.id,
|
||||
i: user.token,
|
||||
};
|
||||
}
|
||||
reply.code(200);
|
||||
return {
|
||||
id: user.id,
|
||||
i: user.token,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -146,6 +146,7 @@ export class SignupApiService {
|
||||
`To complete signup, please click this link: ${link}`);
|
||||
|
||||
reply.code(204);
|
||||
return;
|
||||
} else {
|
||||
try {
|
||||
const { account, secret } = await this.signupService.signup({
|
||||
@@ -162,7 +163,7 @@ export class SignupApiService {
|
||||
token: secret,
|
||||
};
|
||||
} catch (err) {
|
||||
throw new FastifyReplyError(400, err);
|
||||
throw new FastifyReplyError(400, typeof err === 'string' ? err : (err as Error).toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -195,7 +196,7 @@ export class SignupApiService {
|
||||
|
||||
return this.signinService.signin(request, reply, account as ILocalUser);
|
||||
} catch (err) {
|
||||
throw new FastifyReplyError(400, err);
|
||||
throw new FastifyReplyError(400, typeof err === 'string' ? err : (err as Error).toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -20,7 +20,7 @@ export const paramDef = {
|
||||
description: { type: 'string' },
|
||||
color: { type: 'string', nullable: true },
|
||||
iconUrl: { type: 'string', nullable: true },
|
||||
target: { type: 'string' },
|
||||
target: { type: 'string', enum: ['manual', 'conditional'] },
|
||||
condFormula: { type: 'object' },
|
||||
isPublic: { type: 'boolean' },
|
||||
isModerator: { type: 'boolean' },
|
||||
|
@@ -28,7 +28,7 @@ export const paramDef = {
|
||||
description: { type: 'string' },
|
||||
color: { type: 'string', nullable: true },
|
||||
iconUrl: { type: 'string', nullable: true },
|
||||
target: { type: 'string' },
|
||||
target: { type: 'string', enum: ['manual', 'conditional'] },
|
||||
condFormula: { type: 'object' },
|
||||
isPublic: { type: 'boolean' },
|
||||
isModerator: { type: 'boolean' },
|
||||
|
@@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common';
|
||||
import { getJsonSchema } from '@/core/chart/core.js';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import PerUserPvChart from '@/core/chart/charts/per-user-pv.js';
|
||||
import { schema } from '@/core/chart/charts/entities/per-user-notes.js';
|
||||
import { schema } from '@/core/chart/charts/entities/per-user-pv.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['charts', 'users'],
|
||||
|
@@ -27,7 +27,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
return {
|
||||
params: Object.entries(ep.params.properties ?? {}).map(([k, v]) => ({
|
||||
name: k,
|
||||
type: v.type.charAt(0).toUpperCase() + v.type.slice(1),
|
||||
type: v.type ? v.type.charAt(0).toUpperCase() + v.type.slice(1) : 'string',
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { AchievementService } from '@/core/AchievementService.js';
|
||||
import { AchievementService, ACHIEVEMENT_TYPES } from '@/core/AchievementService.js';
|
||||
|
||||
export const meta = {
|
||||
requireCredential: true,
|
||||
@@ -10,7 +10,7 @@ export const meta = {
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string' },
|
||||
name: { type: 'string', enum: ACHIEVEMENT_TYPES },
|
||||
},
|
||||
required: ['name'],
|
||||
} as const;
|
||||
|
@@ -15,8 +15,8 @@ export const meta = {
|
||||
requireCredential: true,
|
||||
|
||||
limit: {
|
||||
duration: 60000,
|
||||
max: 15,
|
||||
duration: 30000,
|
||||
max: 30,
|
||||
},
|
||||
|
||||
kind: 'read:notifications',
|
||||
|
@@ -155,7 +155,7 @@ export class ClientServerService {
|
||||
});
|
||||
|
||||
serverAdapter.setBasePath(bullBoardPath);
|
||||
fastify.register(serverAdapter.registerPlugin(), { prefix: bullBoardPath });
|
||||
(fastify.register as any)(serverAdapter.registerPlugin(), { prefix: bullBoardPath });
|
||||
//#endregion
|
||||
|
||||
fastify.register(fastifyView, {
|
||||
@@ -337,7 +337,7 @@ export class ClientServerService {
|
||||
|
||||
const renderBase = async (reply: FastifyReply) => {
|
||||
const meta = await this.metaService.fetch();
|
||||
reply.header('Cache-Control', 'public, max-age=15');
|
||||
reply.header('Cache-Control', 'public, max-age=30');
|
||||
return await reply.view('base', {
|
||||
img: meta.bannerUrl,
|
||||
title: meta.name ?? 'Misskey',
|
||||
@@ -372,6 +372,7 @@ export class ClientServerService {
|
||||
return feed.atom1();
|
||||
} else {
|
||||
reply.code(404);
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -384,6 +385,7 @@ export class ClientServerService {
|
||||
return feed.rss2();
|
||||
} else {
|
||||
reply.code(404);
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -396,6 +398,7 @@ export class ClientServerService {
|
||||
return feed.json1();
|
||||
} else {
|
||||
reply.code(404);
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
|
@@ -35,7 +35,8 @@ html
|
||||
link(rel='prefetch' href='https://xn--931a.moe/assets/info.jpg')
|
||||
link(rel='prefetch' href='https://xn--931a.moe/assets/not-found.jpg')
|
||||
link(rel='prefetch' href='https://xn--931a.moe/assets/error.jpg')
|
||||
link(rel='stylesheet' href='/assets/tabler-icons/tabler-icons.min.css')
|
||||
//- https://github.com/misskey-dev/misskey/issues/9842
|
||||
link(rel='stylesheet' href='/assets/tabler-icons/tabler-icons.min.css?v2.2.0')
|
||||
link(rel='modulepreload' href=`/vite/${clientEntry.file}`)
|
||||
|
||||
if !config.clientManifestExists
|
||||
|
@@ -12,7 +12,7 @@
|
||||
"@rollup/plugin-json": "6.0.0",
|
||||
"@rollup/pluginutils": "5.0.2",
|
||||
"@syuilo/aiscript": "0.12.4",
|
||||
"@tabler/icons-webfont": "2.1.2",
|
||||
"@tabler/icons-webfont": "2.2.0",
|
||||
"@vitejs/plugin-vue": "4.0.0",
|
||||
"@vue/compiler-sfc": "3.2.47",
|
||||
"autobind-decorator": "2.4.0",
|
||||
@@ -23,7 +23,7 @@
|
||||
"canvas-confetti": "1.6.0",
|
||||
"chart.js": "4.2.0",
|
||||
"chartjs-adapter-date-fns": "3.0.0",
|
||||
"chartjs-chart-matrix": "1.3.0",
|
||||
"chartjs-chart-matrix": "2.0.1",
|
||||
"chartjs-plugin-gradient": "0.6.1",
|
||||
"chartjs-plugin-zoom": "2.0.0",
|
||||
"compare-versions": "5.0.1",
|
||||
@@ -44,7 +44,7 @@
|
||||
"punycode": "2.3.0",
|
||||
"querystring": "0.2.1",
|
||||
"rndstr": "1.0.0",
|
||||
"rollup": "3.12.1",
|
||||
"rollup": "3.14.0",
|
||||
"s-age": "1.1.2",
|
||||
"sanitize-html": "2.9.0",
|
||||
"sass": "1.58.0",
|
||||
@@ -55,7 +55,7 @@
|
||||
"textarea-caret": "3.1.0",
|
||||
"three": "0.149.0",
|
||||
"throttle-debounce": "5.0.0",
|
||||
"tinycolor2": "1.5.2",
|
||||
"tinycolor2": "1.6.0",
|
||||
"tsc-alias": "1.8.2",
|
||||
"tsconfig-paths": "4.1.2",
|
||||
"twemoji-parser": "14.0.0",
|
||||
@@ -74,7 +74,7 @@
|
||||
"@types/gulp": "4.0.10",
|
||||
"@types/gulp-rename": "2.0.1",
|
||||
"@types/matter-js": "0.18.2",
|
||||
"@types/node": "18.11.18",
|
||||
"@types/node": "18.13.0",
|
||||
"@types/punycode": "2.1.0",
|
||||
"@types/sanitize-html": "2.8.0",
|
||||
"@types/seedrandom": "3.0.4",
|
||||
@@ -83,8 +83,8 @@
|
||||
"@types/uuid": "9.0.0",
|
||||
"@types/websocket": "1.0.5",
|
||||
"@types/ws": "8.5.4",
|
||||
"@typescript-eslint/eslint-plugin": "5.50.0",
|
||||
"@typescript-eslint/parser": "5.50.0",
|
||||
"@typescript-eslint/eslint-plugin": "5.51.0",
|
||||
"@typescript-eslint/parser": "5.51.0",
|
||||
"@vue/runtime-core": "3.2.47",
|
||||
"cross-env": "7.0.3",
|
||||
"cypress": "12.5.1",
|
||||
|
@@ -61,8 +61,6 @@ export async function signout() {
|
||||
} catch (err) {}
|
||||
//#endregion
|
||||
|
||||
document.cookie = 'igi=; path=/';
|
||||
|
||||
if (accounts.length > 0) login(accounts[0].token);
|
||||
else unisonReload('/');
|
||||
}
|
||||
|
@@ -4,7 +4,7 @@
|
||||
<MkA v-user-preview="report.targetUserId" class="info" :to="`/user-info/${report.targetUserId}`">
|
||||
<MkAvatar class="avatar" :user="report.targetUser" indicator/>
|
||||
<div class="names">
|
||||
<span class="name _nowrap">{{ report.targetUser.name ?? report.targetUser.username }}</span>
|
||||
<MkUserName class="name" :user="report.targetUser"/>
|
||||
<MkAcct class="acct" :user="report.targetUser" style="display: block;"/>
|
||||
</div>
|
||||
</MkA>
|
||||
|
@@ -4,7 +4,7 @@
|
||||
<li v-for="user in users" tabindex="-1" :class="$style.item" @click="complete(type, user)" @keydown="onKeydown">
|
||||
<img :class="$style.avatar" :src="user.avatarUrl"/>
|
||||
<span :class="$style.userName">
|
||||
<span :key="user.id" class="_nowrap">{{ user.name ?? user.username }}</span>
|
||||
<MkUserName :key="user.id" :user="user"/>
|
||||
</span>
|
||||
<span>@{{ acct(user) }}</span>
|
||||
</li>
|
||||
|
@@ -18,7 +18,7 @@
|
||||
</div>
|
||||
</Transition>
|
||||
<div class="container">
|
||||
<img ref="imgEl" :src="imgUrl" style="display: none;" @load="onImageLoad">
|
||||
<img ref="imgEl" :src="imgUrl" style="display: none;" crossorigin="anonymous" @load="onImageLoad">
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
@@ -42,7 +42,7 @@ import { i18n } from '@/i18n';
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: string | number;
|
||||
type?: 'text' | 'number' | 'password' | 'email' | 'url' | 'date' | 'time' | 'search';
|
||||
type?: 'text' | 'number' | 'password' | 'email' | 'url' | 'date' | 'time' | 'search' | 'datetime-local';
|
||||
required?: boolean;
|
||||
readonly?: boolean;
|
||||
disabled?: boolean;
|
||||
|
@@ -1,22 +1,23 @@
|
||||
<template>
|
||||
<div v-if="hide" class="qjewsnkg" @click="hide = false">
|
||||
<ImgWithBlurhash class="bg" :hash="image.blurhash" :title="image.comment" :alt="image.comment"/>
|
||||
<div class="text">
|
||||
<div class="wrapper">
|
||||
<div v-if="hide" :class="$style.hidden" @click="hide = false">
|
||||
<ImgWithBlurhash style="filter: brightness(0.5);" :hash="image.blurhash" :title="image.comment" :alt="image.comment"/>
|
||||
<div :class="$style.hiddenText">
|
||||
<div :class="$style.hiddenTextWrapper">
|
||||
<b style="display: block;"><i class="ti ti-alert-triangle"></i> {{ $ts.sensitive }}</b>
|
||||
<span style="display: block;">{{ $ts.clickToShow }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="gqnyydlz">
|
||||
<div v-else :class="$style.visible">
|
||||
<a
|
||||
:class="$style.imageContainer"
|
||||
:href="image.url"
|
||||
:title="image.name"
|
||||
>
|
||||
<ImgWithBlurhash :hash="image.blurhash" :src="url" :alt="image.comment || image.name" :title="image.comment || image.name" :cover="false"/>
|
||||
<div v-if="image.type === 'image/gif'" class="gif">GIF</div>
|
||||
<div v-if="image.type === 'image/gif'" :class="$style.gif">GIF</div>
|
||||
</a>
|
||||
<button v-tooltip="$ts.hide" class="_button hide" @click="hide = true"><i class="ti ti-eye-off"></i></button>
|
||||
<button v-tooltip="$ts.hide" :class="$style.hide" class="_button" @click="hide = true"><i class="ti ti-eye-off"></i></button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -49,82 +50,77 @@ watch(() => props.image, () => {
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.qjewsnkg {
|
||||
<style lang="scss" module>
|
||||
.hidden {
|
||||
position: relative;
|
||||
|
||||
> .bg {
|
||||
filter: brightness(0.5);
|
||||
}
|
||||
|
||||
> .text {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
> .wrapper {
|
||||
display: table-cell;
|
||||
text-align: center;
|
||||
font-size: 0.8em;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.gqnyydlz {
|
||||
.hiddenText {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.hiddenTextWrapper {
|
||||
display: table-cell;
|
||||
text-align: center;
|
||||
font-size: 0.8em;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.visible {
|
||||
position: relative;
|
||||
//box-shadow: 0 0 0 1px var(--divider) inset;
|
||||
background: var(--bg);
|
||||
--c: rgb(0 0 0 / 2%);
|
||||
background-image: linear-gradient(45deg, var(--c) 16.67%, var(--bg) 16.67%, var(--bg) 50%, var(--c) 50%, var(--c) 66.67%, var(--bg) 66.67%, var(--bg) 100%);
|
||||
background-size: 16px 16px;
|
||||
}
|
||||
|
||||
> .hide {
|
||||
display: block;
|
||||
position: absolute;
|
||||
border-radius: 6px;
|
||||
background-color: var(--accentedBg);
|
||||
-webkit-backdrop-filter: var(--blur, blur(15px));
|
||||
backdrop-filter: var(--blur, blur(15px));
|
||||
color: var(--accent);
|
||||
font-size: 0.8em;
|
||||
padding: 6px 8px;
|
||||
text-align: center;
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
.hide {
|
||||
display: block;
|
||||
position: absolute;
|
||||
border-radius: 6px;
|
||||
background-color: var(--accentedBg);
|
||||
-webkit-backdrop-filter: var(--blur, blur(15px));
|
||||
backdrop-filter: var(--blur, blur(15px));
|
||||
color: var(--accent);
|
||||
font-size: 0.8em;
|
||||
padding: 6px 8px;
|
||||
text-align: center;
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
}
|
||||
|
||||
> i {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
.imageContainer {
|
||||
display: block;
|
||||
cursor: zoom-in;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-position: center;
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
> a {
|
||||
display: block;
|
||||
cursor: zoom-in;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-position: center;
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
|
||||
> .gif {
|
||||
background-color: var(--fg);
|
||||
border-radius: 6px;
|
||||
color: var(--accentLighten);
|
||||
display: inline-block;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
left: 12px;
|
||||
opacity: .5;
|
||||
padding: 0 6px;
|
||||
text-align: center;
|
||||
top: 12px;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
.gif {
|
||||
background-color: var(--fg);
|
||||
border-radius: 6px;
|
||||
color: var(--accentLighten);
|
||||
display: inline-block;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
left: 12px;
|
||||
opacity: .5;
|
||||
padding: 0 6px;
|
||||
text-align: center;
|
||||
top: 12px;
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
|
@@ -27,7 +27,7 @@
|
||||
<span v-if="item.indicate" :class="$style.indicator"><i class="_indicatorCircle"></i></span>
|
||||
</a>
|
||||
<button v-else-if="item.type === 'user'" :tabindex="i" class="_button" :class="[$style.item, { [$style.active]: item.active }]" :disabled="item.active" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
|
||||
<MkAvatar :user="item.user" :class="$style.avatar"/><span class="_nowrap">{{ item.user.name ?? item.user.username }}</span>
|
||||
<MkAvatar :user="item.user" :class="$style.avatar"/><MkUserName :user="item.user"/>
|
||||
<span v-if="item.indicate" :class="$style.indicator"><i class="_indicatorCircle"></i></span>
|
||||
</button>
|
||||
<span v-else-if="item.type === 'switch'" :tabindex="i" :class="$style.item" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
|
||||
|
@@ -17,7 +17,7 @@
|
||||
<I18n :src="i18n.ts.renotedBy" tag="span" :class="$style.renoteText">
|
||||
<template #user>
|
||||
<MkA v-user-preview="note.userId" :class="$style.renoteUserName" :to="userPage(note.user)">
|
||||
<span class="_nowrap">{{ note.user.name ?? note.user.username }}</span>
|
||||
<MkUserName :user="note.user"/>
|
||||
</MkA>
|
||||
</template>
|
||||
</I18n>
|
||||
@@ -48,7 +48,7 @@
|
||||
<div :class="$style.text">
|
||||
<span v-if="appearNote.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span>
|
||||
<MkA v-if="appearNote.replyId" :class="$style.replyIcon" :to="`/notes/${appearNote.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA>
|
||||
<Mfm v-if="appearNote.text" v-once :text="appearNote.text" :author="appearNote.user" :i="$i" :emoji-urls="appearNote.emojis"/>
|
||||
<Mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$i" :emoji-urls="appearNote.emojis"/>
|
||||
<div v-if="translating || translation" :class="$style.translation">
|
||||
<MkLoading v-if="translating" mini/>
|
||||
<div v-else :class="$style.translated">
|
||||
@@ -108,7 +108,7 @@
|
||||
<I18n :src="i18n.ts.userSaysSomething" tag="small">
|
||||
<template #name>
|
||||
<MkA v-user-preview="appearNote.userId" :to="userPage(appearNote.user)">
|
||||
<span class="_nowrap">{{ appearNote.user.name ?? appearNote.user.username }}</span>
|
||||
<MkUserName :user="appearNote.user"/>
|
||||
</MkA>
|
||||
</template>
|
||||
</I18n>
|
||||
|
@@ -16,7 +16,7 @@
|
||||
<I18n :src="i18n.ts.renotedBy" tag="span">
|
||||
<template #user>
|
||||
<MkA v-user-preview="note.userId" class="name" :to="userPage(note.user)">
|
||||
<span class="_nowrap">{{ note.user.name ?? note.user.username }}</span>
|
||||
<MkUserName :user="note.user"/>
|
||||
</MkA>
|
||||
</template>
|
||||
</I18n>
|
||||
@@ -39,7 +39,7 @@
|
||||
<div class="body">
|
||||
<div class="top">
|
||||
<MkA v-user-preview="appearNote.user.id" class="name" :to="userPage(appearNote.user)">
|
||||
<span class="_nowrap">{{ appearNote.user.name ?? appearNote.user.username }}</span>
|
||||
<MkUserName :nowrap="false" :user="appearNote.user"/>
|
||||
</MkA>
|
||||
<span v-if="appearNote.user.isBot" class="is-bot">bot</span>
|
||||
<div class="info">
|
||||
@@ -125,7 +125,7 @@
|
||||
<I18n :src="i18n.ts.userSaysSomething" tag="small">
|
||||
<template #name>
|
||||
<MkA v-user-preview="appearNote.userId" class="name" :to="userPage(appearNote.user)">
|
||||
<span class="_nowrap">{{ appearNote.user.name ?? appearNote.user.username }}</span>
|
||||
<MkUserName :user="appearNote.user"/>
|
||||
</MkA>
|
||||
</template>
|
||||
</I18n>
|
||||
|
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<header :class="$style.root">
|
||||
<MkA v-once v-user-preview="note.user.id" :class="$style.name" :to="userPage(note.user)">
|
||||
<span class="_nowrap">{{ note.user.name ?? note.user.username }}</span>
|
||||
<MkUserName :user="note.user"/>
|
||||
</MkA>
|
||||
<div v-if="note.user.isBot" :class="$style.isBot">bot</div>
|
||||
<div :class="$style.username"><MkAcct :user="note.user"/></div>
|
||||
|
@@ -3,7 +3,7 @@
|
||||
<MkAvatar :class="$style.avatar" :user="$i" link preview/>
|
||||
<div :class="$style.main">
|
||||
<div :class="$style.header">
|
||||
<span class="_nowrap">{{ $i.name ?? $i.username }}</span>
|
||||
<MkUserName :user="$i"/>
|
||||
</div>
|
||||
<div>
|
||||
<div :class="$style.content">
|
||||
|
@@ -31,7 +31,7 @@
|
||||
<header :class="$style.header">
|
||||
<span v-if="notification.type === 'pollEnded'">{{ i18n.ts._notification.pollEnded }}</span>
|
||||
<span v-else-if="notification.type === 'achievementEarned'">{{ i18n.ts._notification.achievementEarned }}</span>
|
||||
<MkA v-else-if="notification.user" v-user-preview="notification.user.id" :class="$style.headerName" :to="userPage(notification.user)"><span class="_nowrap">{{ notification.user.name ?? notification.user.username }}</span></MkA>
|
||||
<MkA v-else-if="notification.user" v-user-preview="notification.user.id" :class="$style.headerName" :to="userPage(notification.user)"><MkUserName :user="notification.user"/></MkA>
|
||||
<span v-else>{{ notification.header }}</span>
|
||||
<MkTime v-if="withTime" :time="notification.createdAt" :class="$style.headerTime"/>
|
||||
</header>
|
||||
@@ -215,7 +215,7 @@ useTooltip(reactionRef, (showing) => {
|
||||
border-radius: 100%;
|
||||
background: var(--panel);
|
||||
box-shadow: 0 0 0 3px var(--panel);
|
||||
font-size: 12px;
|
||||
font-size: 11px;
|
||||
text-align: center;
|
||||
color: #fff;
|
||||
|
||||
|
@@ -8,7 +8,7 @@
|
||||
<div :class="$style.users">
|
||||
<div v-for="u in users" :key="u.id" :class="$style.user">
|
||||
<MkAvatar :class="$style.avatar" :user="u"/>
|
||||
<span class="_nowrap">{{ u.name ?? u.username }}</span>
|
||||
<MkUserName :user="u" :nowrap="true"/>
|
||||
</div>
|
||||
<div v-if="users.length > 10">+{{ count - 10 }}</div>
|
||||
</div>
|
||||
|
@@ -4,7 +4,7 @@
|
||||
<span v-if="note.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span>
|
||||
<span v-if="note.deletedAt" style="opacity: 0.5">({{ i18n.ts.deleted }})</span>
|
||||
<MkA v-if="note.replyId" :class="$style.reply" :to="`/notes/${note.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA>
|
||||
<Mfm v-if="note.text" v-once :text="note.text" :author="note.user" :i="$i" :emoji-urls="note.emojis"/>
|
||||
<Mfm v-if="note.text" :text="note.text" :author="note.user" :i="$i" :emoji-urls="note.emojis"/>
|
||||
<MkA v-if="note.renoteId" :class="$style.rp" :to="`/notes/${note.renoteId}`">RN: ...</MkA>
|
||||
</div>
|
||||
<details v-if="note.files.length > 0">
|
||||
|
@@ -2,7 +2,7 @@
|
||||
<div v-adaptive-bg :class="[$style.root, { yellow: user.isSilenced, red: user.isSuspended, gray: false }]">
|
||||
<MkAvatar class="avatar" :user="user" indicator/>
|
||||
<div class="body">
|
||||
<span class="name _nowrap">{{ user.name ?? user.username }}</span>
|
||||
<span class="name"><MkUserName class="name" :user="user"/></span>
|
||||
<span class="sub"><span class="acct _monospace">@{{ acct(user) }}</span></span>
|
||||
</div>
|
||||
<MkMiniChart v-if="chartValues" class="chart" :src="chartValues"/>
|
||||
|
@@ -3,9 +3,8 @@
|
||||
<div class="banner" :style="user.bannerUrl ? `background-image: url(${user.bannerUrl})` : ''"></div>
|
||||
<MkAvatar class="avatar" :user="user" indicator/>
|
||||
<div class="title">
|
||||
<MkA class="name _nowrap" :to="userPage(user)"{{ user.name ?? user.username }}</mk-a>
|
||||
<p class="username"><MkAcct :user="user"/></p>
|
||||
</mka>
|
||||
<MkA class="name" :to="userPage(user)"><MkUserName :user="user" :nowrap="false"/></MkA>
|
||||
<p class="username"><MkAcct :user="user"/></p>
|
||||
</div>
|
||||
<span v-if="$i && $i.id !== user.id && user.isFollowed" class="followed">{{ $ts.followsYou }}</span>
|
||||
<div class="description">
|
||||
|
@@ -7,7 +7,7 @@
|
||||
</div>
|
||||
<MkAvatar class="avatar" :user="user" indicator/>
|
||||
<div class="title">
|
||||
<MkA class="name _nowrap" :to="userPage(user)">{{ user.name ?? user.username }}</MkA>
|
||||
<MkA class="name" :to="userPage(user)"><MkUserName :user="user" :nowrap="false"/></MkA>
|
||||
<p class="username"><MkAcct :user="user"/></p>
|
||||
</div>
|
||||
<div class="description">
|
||||
|
@@ -27,7 +27,7 @@
|
||||
<div v-for="user in users" :key="user.id" class="_button" :class="[$style.user, { [$style.selected]: selected && selected.id === user.id }]" @click="selected = user" @dblclick="ok()">
|
||||
<MkAvatar :user="user" :class="$style.avatar" indicator/>
|
||||
<div :class="$style.userBody">
|
||||
<span :class="$style.userName" class="_nowrap">{{ user.name ?? user.username }}</span>
|
||||
<MkUserName :user="user" :class="$style.userName"/>
|
||||
<MkAcct :user="user" :class="$style.userAcct"/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -41,7 +41,7 @@
|
||||
<div v-for="user in recentUsers" :key="user.id" class="_button" :class="[$style.user, { [$style.selected]: selected && selected.id === user.id }]" @click="selected = user" @dblclick="ok()">
|
||||
<MkAvatar :user="user" :class="$style.avatar" indicator/>
|
||||
<div :class="$style.userBody">
|
||||
<span :class="$style.userName" class="_nowrap">{{ user.name ?? user.username }}</span>
|
||||
<MkUserName :user="user" :class="$style.userName"/>
|
||||
<MkAcct :user="user" :class="$style.userAcct"/>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -3,7 +3,7 @@
|
||||
<div :class="$style.root">
|
||||
<div v-for="u in users" :key="u.id" :class="$style.user">
|
||||
<MkAvatar :class="$style.avatar" :user="u"/>
|
||||
<span :class="$style.name" class="_nowrap">{{ u.name ?? u.username }}</span>
|
||||
<MkUserName :class="$style.name" :user="u" :nowrap="true"/>
|
||||
</div>
|
||||
<div v-if="users.length < count" :class="$style.omitted">+{{ count - users.length }}</div>
|
||||
</div>
|
||||
|
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<MfmCore :text="text" :plain="plain" :nowrap="nowrap" :author="author" :is-note="isNote" :class="[$style.root, { '_nowrap': nowrap }]"/>
|
||||
<MfmCore :text="text" :plain="plain" :nowrap="nowrap" :author="author" :is-note="isNote" :class="[$style.root, { [$style.nowrap]: nowrap }]"/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
@@ -160,5 +160,12 @@ const props = withDefaults(defineProps<{
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
white-space: pre-wrap;
|
||||
|
||||
&.nowrap {
|
||||
white-space: pre;
|
||||
word-wrap: normal; // https://codeday.me/jp/qa/20190424/690106.html
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@@ -9,7 +9,7 @@
|
||||
<i v-else-if="metadata.icon" :class="[$style.titleIcon, metadata.icon]"></i>
|
||||
|
||||
<div :class="$style.title">
|
||||
<span v-if="metadata.userName" class="_nowrap">{{ metadata.userName.name ?? metadata.userName.username }}</span>
|
||||
<MkUserName v-if="metadata.userName" :user="metadata.userName" :nowrap="true"/>
|
||||
<div v-else-if="metadata.title">{{ metadata.title }}</div>
|
||||
<div v-if="!narrow && metadata.subtitle" :class="$style.subtitle">
|
||||
{{ metadata.subtitle }}
|
||||
|
15
packages/frontend/src/components/global/MkUserName.vue
Normal file
15
packages/frontend/src/components/global/MkUserName.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<template>
|
||||
<Mfm :text="user.name ?? user.username" :author="user" :plain="true" :nowrap="nowrap" :emoji-urls="user.emojis"/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import * as misskey from 'misskey-js';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
user: misskey.entities.User;
|
||||
nowrap?: boolean;
|
||||
}>(), {
|
||||
nowrap: true,
|
||||
});
|
||||
</script>
|
@@ -6,6 +6,7 @@ import MkAcct from './global/MkAcct.vue';
|
||||
import MkAvatar from './global/MkAvatar.vue';
|
||||
import MkEmoji from './global/MkEmoji.vue';
|
||||
import MkCustomEmoji from './global/MkCustomEmoji.vue';
|
||||
import MkUserName from './global/MkUserName.vue';
|
||||
import MkEllipsis from './global/MkEllipsis.vue';
|
||||
import MkTime from './global/MkTime.vue';
|
||||
import MkUrl from './global/MkUrl.vue';
|
||||
@@ -27,6 +28,7 @@ export default function(app: App) {
|
||||
app.component('MkAvatar', MkAvatar);
|
||||
app.component('MkEmoji', MkEmoji);
|
||||
app.component('MkCustomEmoji', MkCustomEmoji);
|
||||
app.component('MkUserName', MkUserName);
|
||||
app.component('MkEllipsis', MkEllipsis);
|
||||
app.component('MkTime', MkTime);
|
||||
app.component('MkUrl', MkUrl);
|
||||
@@ -48,6 +50,7 @@ declare module '@vue/runtime-core' {
|
||||
MkAvatar: typeof MkAvatar;
|
||||
MkEmoji: typeof MkEmoji;
|
||||
MkCustomEmoji: typeof MkCustomEmoji;
|
||||
MkUserName: typeof MkUserName;
|
||||
MkEllipsis: typeof MkEllipsis;
|
||||
MkTime: typeof MkTime;
|
||||
MkUrl: typeof MkUrl;
|
||||
|
@@ -35,6 +35,9 @@ export const apiWithDialog = ((
|
||||
} else if (err.code.startsWith('TOO_MANY')) {
|
||||
title = i18n.ts.youCannotCreateAnymore;
|
||||
text = `${i18n.ts.error}: ${err.id}`;
|
||||
} else if (err.message.startsWith('Unexpected token')) {
|
||||
title = i18n.ts.gotInvalidResponseError;
|
||||
text = i18n.ts.gotInvalidResponseErrorDescription;
|
||||
}
|
||||
alert({
|
||||
type: 'error',
|
||||
|
@@ -73,7 +73,13 @@
|
||||
</FormSection>
|
||||
<FormSection>
|
||||
<template #label><Mfm text="$[jelly ❤]"/> {{ i18n.ts._aboutMisskey.patrons }}</template>
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); grid-gap: 12px;">
|
||||
<div :class="$style.patronsWithIcon">
|
||||
<div v-for="patron in patronsWithIcon" :class="$style.patronWithIcon">
|
||||
<img :src="patron.icon" :class="$style.patronIcon">
|
||||
<span :class="$style.patronName">{{ patron.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top: 16px; display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); grid-gap: 12px;">
|
||||
<div v-for="patron in patrons" :key="patron">{{ patron }}</div>
|
||||
</div>
|
||||
<p>{{ i18n.ts._aboutMisskey.morePatrons }}</p>
|
||||
@@ -99,6 +105,14 @@ import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
import { claimAchievement, claimedAchievements } from '@/scripts/achievements';
|
||||
import { $i } from '@/account';
|
||||
|
||||
const patronsWithIcon = [{
|
||||
name: 'カイヤン',
|
||||
icon: 'https://misskey-hub.net/patrons/a2820716883e408cb87773e377ce7c8d.jpg',
|
||||
}, {
|
||||
name: 'だれかさん',
|
||||
icon: 'https://misskey-hub.net/patrons/f7409b5e5a88477a9b9d740c408de125.jpg',
|
||||
}];
|
||||
|
||||
const patrons = [
|
||||
'まっちゃとーにゅ',
|
||||
'mametsuko',
|
||||
@@ -352,4 +366,27 @@ definePageMetadata({
|
||||
.contributorUsername {
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
.patronsWithIcon {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
grid-gap: 12px;
|
||||
}
|
||||
|
||||
.patronWithIcon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px;
|
||||
background: var(--buttonBg);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.patronIcon {
|
||||
width: 24px;
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
||||
.patronName {
|
||||
margin-left: 12px;
|
||||
}
|
||||
</style>
|
||||
|
@@ -29,7 +29,7 @@
|
||||
<MkInput v-model="ad.ratio" type="number">
|
||||
<template #label>{{ i18n.ts.ratio }}</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="ad.expiresAt" type="date">
|
||||
<MkInput v-model="ad.expiresAt" type="datetime-local">
|
||||
<template #label>{{ i18n.ts.expiration }}</template>
|
||||
</MkInput>
|
||||
</FormSplit>
|
||||
@@ -61,7 +61,12 @@ import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
let ads: any[] = $ref([]);
|
||||
|
||||
os.api('admin/ad/list').then(adsResponse => {
|
||||
ads = adsResponse;
|
||||
ads = adsResponse.map(r => {
|
||||
return {
|
||||
...r,
|
||||
expiresAt: new Date(r.expiresAt).toISOString().slice(0, 16),
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
function add() {
|
||||
|
@@ -77,6 +77,8 @@ export default defineComponent({
|
||||
accepted() {
|
||||
this.state = 'accepted';
|
||||
if (this.session.app.callbackUrl) {
|
||||
const url = new URL(this.session.app.callbackUrl);
|
||||
if (['javascript:', 'file:', 'data:', 'mailto:', 'tel:'].includes(url.protocol)) throw new Error('invalid url');
|
||||
location.href = `${this.session.app.callbackUrl}?token=${this.session.token}`;
|
||||
}
|
||||
}, onLogin(res) {
|
||||
|
@@ -23,7 +23,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<XPostForm v-if="$i" :channel="channel" class="post-form _panel _margin" fixed/>
|
||||
<MkPostForm v-if="$i" :channel="channel" class="post-form _panel _margin" fixed/>
|
||||
|
||||
<XTimeline :key="channelId" class="_margin" src="channel" :channel="channelId" @before="before" @after="after"/>
|
||||
</div>
|
||||
@@ -34,7 +34,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, inject, watch } from 'vue';
|
||||
import MkContainer from '@/components/MkContainer.vue';
|
||||
import XPostForm from '@/components/MkPostForm.vue';
|
||||
import MkPostForm from '@/components/MkPostForm.vue';
|
||||
import XTimeline from '@/components/MkTimeline.vue';
|
||||
import XChannelFollowButton from '@/components/MkChannelFollowButton.vue';
|
||||
import * as os from '@/os';
|
||||
|
@@ -8,7 +8,7 @@
|
||||
<Mfm :text="clip.description" :is-note="false" :i="$i"/>
|
||||
</div>
|
||||
<div class="user">
|
||||
<MkAvatar :user="clip.user" class="avatar" indicator link preview/> <span class="_nowrap">{{ clip.user.name ?? clip.user.username }}</span>
|
||||
<MkAvatar :user="clip.user" class="avatar" indicator link preview/> <MkUserName :user="clip.user" :nowrap="false"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@@ -15,7 +15,7 @@
|
||||
<MkAvatar class="avatar" :user="req.follower" indicator link preview/>
|
||||
<div class="body">
|
||||
<div class="name">
|
||||
<MkA v-user-preview="req.follower.id" class="name _nowrap" :to="userPage(req.follower)">{{ req.follower.name ?? req.follower.username }}</MkA>
|
||||
<MkA v-user-preview="req.follower.id" class="name" :to="userPage(req.follower)"><MkUserName :user="req.follower"/></MkA>
|
||||
<p class="acct">@{{ acct(req.follower) }}</p>
|
||||
</div>
|
||||
<div v-if="req.follower.description" class="description" :title="req.follower.description">
|
||||
|
@@ -30,7 +30,7 @@
|
||||
<div class="user">
|
||||
<MkAvatar :user="post.user" class="avatar" link preview/>
|
||||
<div class="name">
|
||||
<div class="_nowrap">{{ post.user.name ?? post.user.username }}</div>
|
||||
<MkUserName :user="post.user" style="display: block;"/>
|
||||
<MkAcct :user="post.user"/>
|
||||
</div>
|
||||
<MkFollowButton v-if="!$i || $i.id != post.user.id" :user="post.user" :inline="true" :transparent="false" :full="true" large class="koudoku"/>
|
||||
|
@@ -22,7 +22,7 @@
|
||||
<MkTime :time="message.createdAt" class="time"/>
|
||||
</header>
|
||||
<header v-else>
|
||||
<span class="name">{{ isMe(message) ? message.recipient.name : message.user.name }}</span>
|
||||
<span class="name"><MkUserName :user="isMe(message) ? message.recipient : message.user"/></span>
|
||||
<span class="username">@{{ acct(isMe(message) ? message.recipient : message.user) }}</span>
|
||||
<MkTime :time="message.createdAt" class="time"/>
|
||||
</header>
|
||||
|
@@ -70,7 +70,7 @@ async function accept(): Promise<void> {
|
||||
state = 'accepted';
|
||||
if (props.callback) {
|
||||
const cbUrl = new URL(props.callback);
|
||||
if (!['http:', 'https:'].includes(cbUrl.protocol)) throw new Error('invalid url');
|
||||
if (['javascript:', 'file:', 'data:', 'mailto:', 'tel:'].includes(cbUrl.protocol)) throw new Error('invalid url');
|
||||
cbUrl.searchParams.set('session', props.session);
|
||||
location.href = cbUrl.href;
|
||||
}
|
||||
|
@@ -21,7 +21,7 @@
|
||||
<div v-for="user in users" :key="user.id" class="user _panel">
|
||||
<MkAvatar :user="user" class="avatar" indicator link preview/>
|
||||
<div class="body">
|
||||
<span class="name _nowrap">{{ user.name ?? user.username }}</span>
|
||||
<MkUserName :user="user" class="name"/>
|
||||
<MkAcct :user="user" class="acct"/>
|
||||
</div>
|
||||
<div class="action">
|
||||
|
@@ -21,7 +21,7 @@
|
||||
<b>{{ item.name }}</b>
|
||||
<div v-if="item.description" class="description">{{ item.description }}</div>
|
||||
<div class="user">
|
||||
<MkAvatar :user="item.user" class="avatar" indicator link preview/> <span class="_nowrap">{{ item.user.name ?? item.user.username }}</span>
|
||||
<MkAvatar :user="item.user" class="avatar" indicator link preview/> <MkUserName :user="item.user" :nowrap="false"/>
|
||||
</div>
|
||||
</MkA>
|
||||
</div>
|
||||
|
@@ -29,7 +29,7 @@
|
||||
<div class="user">
|
||||
<MkAvatar :user="page.user" class="avatar" link preview/>
|
||||
<div class="name">
|
||||
<div class="_nowrap">{{ page.user.name ?? page.user.username }}</div>
|
||||
<MkUserName :user="page.user" style="display: block;"/>
|
||||
<MkAcct :user="page.user"/>
|
||||
</div>
|
||||
<MkFollowButton v-if="!$i || $i.id != page.user.id" :user="page.user" :inline="true" :transparent="false" :full="true" large class="koudoku"/>
|
||||
|
@@ -9,7 +9,9 @@
|
||||
<MkAvatar :user="account" class="avatar"/>
|
||||
</div>
|
||||
<div class="body">
|
||||
<div class="name _nowrap">{{ account.name ?? account.username }}</div>
|
||||
<div class="name">
|
||||
<MkUserName :user="account"/>
|
||||
</div>
|
||||
<div class="acct">
|
||||
<MkAcct :user="account"/>
|
||||
</div>
|
||||
|
@@ -91,7 +91,7 @@ function regenerateToken() {
|
||||
type: 'password',
|
||||
}).then(({ canceled, result: password }) => {
|
||||
if (canceled) return;
|
||||
os.api('i/regenerate_token', {
|
||||
os.api('i/regenerate-token', {
|
||||
password: password,
|
||||
});
|
||||
});
|
||||
|
@@ -29,7 +29,7 @@ import { noteVisibilities } from 'misskey-js';
|
||||
import * as Acct from 'misskey-js/built/acct';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import XPostForm from '@/components/MkPostForm.vue';
|
||||
import MkPostForm from '@/components/MkPostForm.vue';
|
||||
import * as os from '@/os';
|
||||
import { mainRouter } from '@/router';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
@@ -69,14 +69,14 @@ async function init() {
|
||||
...(visibleAccts ? visibleAccts.split(',').map(Acct.parse) : []),
|
||||
]
|
||||
// TypeScriptの指示通りに変換する
|
||||
.map(q => 'username' in q ? { username: q.username, host: q.host === null ? undefined : q.host } : q)
|
||||
.map(q => os.api('users/show', q)
|
||||
.then(user => {
|
||||
visibleUsers.push(user);
|
||||
}, () => {
|
||||
console.error(`Invalid user query: ${JSON.stringify(q)}`);
|
||||
}),
|
||||
),
|
||||
.map(q => 'username' in q ? { username: q.username, host: q.host === null ? undefined : q.host } : q)
|
||||
.map(q => os.api('users/show', q)
|
||||
.then(user => {
|
||||
visibleUsers.push(user);
|
||||
}, () => {
|
||||
console.error(`Invalid user query: ${JSON.stringify(q)}`);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -120,13 +120,13 @@ async function init() {
|
||||
if (fileIds) {
|
||||
await Promise.all(
|
||||
fileIds.split(',')
|
||||
.map(fileId => os.api('drive/files/show', { fileId })
|
||||
.then(file => {
|
||||
files.push(file);
|
||||
}, () => {
|
||||
console.error(`Failed to fetch a file ${fileId}`);
|
||||
}),
|
||||
),
|
||||
.map(fileId => os.api('drive/files/show', { fileId })
|
||||
.then(file => {
|
||||
files.push(file);
|
||||
}, () => {
|
||||
console.error(`Failed to fetch a file ${fileId}`);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
//#endregion
|
||||
|
@@ -4,7 +4,7 @@
|
||||
<MkSpacer :content-max="800">
|
||||
<div ref="rootEl" v-hotkey.global="keymap">
|
||||
<XTutorial v-if="$i && $store.reactiveState.tutorial.value != -1" class="_panel" style="margin-bottom: var(--margin);"/>
|
||||
<XPostForm v-if="$store.reactiveState.showFixedPostForm.value" :class="$style.postForm" class="post-form _panel" fixed style="margin-bottom: var(--margin);"/>
|
||||
<MkPostForm v-if="$store.reactiveState.showFixedPostForm.value" :class="$style.postForm" class="post-form _panel" fixed style="margin-bottom: var(--margin);"/>
|
||||
|
||||
<div v-if="queue > 0" :class="$style.new"><button class="_buttonPrimary" @click="top()">{{ i18n.ts.newNoteRecived }}</button></div>
|
||||
<div :class="$style.tl">
|
||||
@@ -24,7 +24,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { defineAsyncComponent, computed, watch } from 'vue';
|
||||
import XTimeline from '@/components/MkTimeline.vue';
|
||||
import XPostForm from '@/components/MkPostForm.vue';
|
||||
import MkPostForm from '@/components/MkPostForm.vue';
|
||||
import { scroll } from '@/scripts/scroll';
|
||||
import * as os from '@/os';
|
||||
import { defaultStore } from '@/store';
|
||||
|
@@ -7,7 +7,7 @@
|
||||
<div class="aeakzknw">
|
||||
<MkAvatar class="avatar" :user="user" indicator link preview/>
|
||||
<div class="body">
|
||||
<span class="name _nowrap">{{ user.name ?? user.username }}</span>
|
||||
<span class="name"><MkUserName class="name" :user="user"/></span>
|
||||
<span class="sub"><span class="acct _monospace">@{{ acct(user) }}</span></span>
|
||||
<span class="state">
|
||||
<span v-if="suspended" class="suspended">Suspended</span>
|
||||
|
@@ -14,7 +14,7 @@
|
||||
<div ref="bannerEl" class="banner" :style="style"></div>
|
||||
<div class="fade"></div>
|
||||
<div class="title">
|
||||
<span class="name _nowrap">{{ user.name ?? user.username }}</span>
|
||||
<MkUserName class="name" :user="user" :nowrap="true"/>
|
||||
<div class="bottom">
|
||||
<span class="username"><MkAcct :user="user" :detail="true"/></span>
|
||||
<span v-if="user.isAdmin" :title="i18n.ts.isAdmin" style="color: var(--badge);"><i class="ti ti-shield"></i></span>
|
||||
@@ -30,7 +30,7 @@
|
||||
</div>
|
||||
<MkAvatar class="avatar" :user="user" indicator/>
|
||||
<div class="title">
|
||||
<span class="name _nowrap">{{ user.name ?? user.username }}</span>
|
||||
<MkUserName :user="user" :nowrap="false" class="name"/>
|
||||
<div class="bottom">
|
||||
<span class="username"><MkAcct :user="user" :detail="true"/></span>
|
||||
<span v-if="user.isAdmin" :title="i18n.ts.isAdmin" style="color: var(--badge);"><i class="ti ti-shield"></i></span>
|
||||
|
@@ -127,13 +127,11 @@ hr {
|
||||
}
|
||||
|
||||
.ti {
|
||||
vertical-align: -40%;
|
||||
vertical-align: -12%;
|
||||
line-height: 1em;
|
||||
|
||||
&:before {
|
||||
display: inline-block;
|
||||
font-size: 165%;
|
||||
width: 0.74em;
|
||||
font-size: 128%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,9 +155,9 @@ hr {
|
||||
}
|
||||
|
||||
._nowrap {
|
||||
white-space: pre;
|
||||
word-wrap: normal; // https://codeday.me/jp/qa/20190424/690106.html
|
||||
overflow: clip;
|
||||
white-space: pre !important;
|
||||
word-wrap: normal !important; // https://codeday.me/jp/qa/20190424/690106.html
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
|
@@ -146,6 +146,7 @@ const addColumn = async (ev) => {
|
||||
'tl',
|
||||
'antenna',
|
||||
'list',
|
||||
'channel',
|
||||
'mentions',
|
||||
'direct',
|
||||
];
|
||||
|
71
packages/frontend/src/ui/deck/channel-column.vue
Normal file
71
packages/frontend/src/ui/deck/channel-column.vue
Normal file
@@ -0,0 +1,71 @@
|
||||
<template>
|
||||
<XColumn :menu="menu" :column="column" :is-stacked="isStacked" @parent-focus="$event => emit('parent-focus', $event)">
|
||||
<template #header>
|
||||
<i class="ti ti-device-tv"></i><span style="margin-left: 8px;">{{ column.name }}</span>
|
||||
</template>
|
||||
|
||||
<template v-if="column.channelId">
|
||||
<div style="padding: 8px; text-align: center;">
|
||||
<MkButton primary gradate rounded inline @click="post"><i class="ti ti-pencil"></i></MkButton>
|
||||
</div>
|
||||
<XTimeline ref="timeline" src="channel" :channel="column.channelId" @after="() => emit('loaded')"/>
|
||||
</template>
|
||||
</XColumn>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import XColumn from './column.vue';
|
||||
import { updateColumn, Column } from './deck-store';
|
||||
import XTimeline from '@/components/MkTimeline.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import * as os from '@/os';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
const props = defineProps<{
|
||||
column: Column;
|
||||
isStacked: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'loaded'): void;
|
||||
(ev: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void;
|
||||
}>();
|
||||
|
||||
let timeline = $shallowRef<InstanceType<typeof XTimeline>>();
|
||||
|
||||
if (props.column.channelId == null) {
|
||||
setChannel();
|
||||
}
|
||||
|
||||
async function setChannel() {
|
||||
const channels = await os.api('channels/followed');
|
||||
const { canceled, result: channel } = await os.select({
|
||||
title: i18n.ts.selectChannel,
|
||||
items: channels.map(x => ({
|
||||
value: x, text: x.name,
|
||||
})),
|
||||
default: props.column.channelId,
|
||||
});
|
||||
if (canceled) return;
|
||||
updateColumn(props.column.id, {
|
||||
channelId: channel.id,
|
||||
name: channel.name,
|
||||
});
|
||||
}
|
||||
|
||||
function post() {
|
||||
os.post({
|
||||
channel: {
|
||||
id: props.column.channelId,
|
||||
},
|
||||
instant: true,
|
||||
});
|
||||
}
|
||||
|
||||
const menu = [{
|
||||
icon: 'ti ti-pencil',
|
||||
text: i18n.ts.selectChannel,
|
||||
action: setChannel,
|
||||
}];
|
||||
</script>
|
@@ -6,6 +6,7 @@
|
||||
<XNotificationsColumn v-else-if="column.type === 'notifications'" :column="column" :is-stacked="isStacked" @parent-focus="emit('parent-focus', $event)"/>
|
||||
<XTlColumn v-else-if="column.type === 'tl'" :column="column" :is-stacked="isStacked" @parent-focus="emit('parent-focus', $event)"/>
|
||||
<XListColumn v-else-if="column.type === 'list'" :column="column" :is-stacked="isStacked" @parent-focus="emit('parent-focus', $event)"/>
|
||||
<XChannelColumn v-else-if="column.type === 'channel'" :column="column" :is-stacked="isStacked" @parent-focus="emit('parent-focus', $event)"/>
|
||||
<XAntennaColumn v-else-if="column.type === 'antenna'" :column="column" :is-stacked="isStacked" @parent-focus="emit('parent-focus', $event)"/>
|
||||
<XMentionsColumn v-else-if="column.type === 'mentions'" :column="column" :is-stacked="isStacked" @parent-focus="emit('parent-focus', $event)"/>
|
||||
<XDirectColumn v-else-if="column.type === 'direct'" :column="column" :is-stacked="isStacked" @parent-focus="emit('parent-focus', $event)"/>
|
||||
@@ -17,6 +18,7 @@ import XMainColumn from './main-column.vue';
|
||||
import XTlColumn from './tl-column.vue';
|
||||
import XAntennaColumn from './antenna-column.vue';
|
||||
import XListColumn from './list-column.vue';
|
||||
import XChannelColumn from './channel-column.vue';
|
||||
import XNotificationsColumn from './notifications-column.vue';
|
||||
import XWidgetsColumn from './widgets-column.vue';
|
||||
import XMentionsColumn from './mentions-column.vue';
|
||||
|
@@ -14,7 +14,7 @@ type ColumnWidget = {
|
||||
|
||||
export type Column = {
|
||||
id: string;
|
||||
type: 'main' | 'widgets' | 'notifications' | 'tl' | 'antenna' | 'list' | 'mentions' | 'direct';
|
||||
type: 'main' | 'widgets' | 'notifications' | 'tl' | 'antenna' | 'channel' | 'list' | 'mentions' | 'direct';
|
||||
name: string | null;
|
||||
width: number;
|
||||
widgets?: ColumnWidget[];
|
||||
@@ -22,6 +22,7 @@ export type Column = {
|
||||
flexible?: boolean;
|
||||
antennaId?: string;
|
||||
listId?: string;
|
||||
channelId?: string;
|
||||
includingTypes?: typeof notificationTypes[number][];
|
||||
tl?: 'home' | 'local' | 'social' | 'global';
|
||||
};
|
||||
|
@@ -142,10 +142,10 @@ mainRouter.on('change', () => {
|
||||
document.documentElement.style.overflowY = 'scroll';
|
||||
|
||||
if (window.innerWidth > 1024) {
|
||||
const tempUI = miLocalStorage.getItem('ui_temp')
|
||||
const tempUI = miLocalStorage.getItem('ui_temp');
|
||||
if (tempUI) {
|
||||
miLocalStorage.setItem('ui', tempUI)
|
||||
miLocalStorage.removeItem('ui_temp')
|
||||
miLocalStorage.setItem('ui', tempUI);
|
||||
miLocalStorage.removeItem('ui_temp');
|
||||
location.reload();
|
||||
}
|
||||
}
|
||||
|
@@ -1,12 +1,12 @@
|
||||
<template>
|
||||
<XPostForm class="_panel mkw-postForm data-cy-mkw-postForm" :fixed="true" :autofocus="false"/>
|
||||
<MkPostForm class="_panel mkw-post-form data-cy-mkw-postForm" :fixed="true" :autofocus="false"/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import { GetFormResultType } from '@/scripts/form';
|
||||
import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget';
|
||||
import XPostForm from '@/components/MkPostForm.vue';
|
||||
import { GetFormResultType } from '@/scripts/form';
|
||||
import MkPostForm from '@/components/MkPostForm.vue';
|
||||
|
||||
const name = 'postForm';
|
||||
|
||||
|
@@ -6,7 +6,9 @@
|
||||
</div>
|
||||
<div :class="$style.bodyContainer">
|
||||
<div :class="$style.body">
|
||||
<MkA :class="$style.name" :to="userPage($i)" class="_nowrap">{{ $i.name ?? $i.username }}</MkA>
|
||||
<MkA :class="$style.name" :to="userPage($i)">
|
||||
<MkUserName :user="$i"/>
|
||||
</MkA>
|
||||
<div :class="$style.username"><MkAcct :user="$i" detail/></div>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -12,8 +12,8 @@
|
||||
"misskey-js": "0.0.15"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@typescript-eslint/parser": "5.50.0",
|
||||
"@typescript/lib-webworker": "npm:@types/serviceworker@0.0.61",
|
||||
"@typescript-eslint/parser": "5.51.0",
|
||||
"@typescript/lib-webworker": "npm:@types/serviceworker@0.0.62",
|
||||
"eslint": "8.33.0",
|
||||
"eslint-plugin-import": "2.27.5",
|
||||
"typescript": "4.9.5"
|
||||
|
1098
pnpm-lock.yaml
generated
1098
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user