Compare commits
23 Commits
13.13.0-be
...
13.13.0-be
Author | SHA1 | Date | |
---|---|---|---|
![]() |
ca75afe065 | ||
![]() |
915ed39715 | ||
![]() |
81fd94e635 | ||
![]() |
05507a4bea | ||
![]() |
d177f97928 | ||
![]() |
30cb03a40d | ||
![]() |
c685989e67 | ||
![]() |
ee3f408c7d | ||
![]() |
1eb35dd5bc | ||
![]() |
15db0b8812 | ||
![]() |
1b78c6a309 | ||
![]() |
c713af8e23 | ||
![]() |
bd6666173a | ||
![]() |
02715f5d14 | ||
![]() |
acd5e0b8f6 | ||
![]() |
be2142bb13 | ||
![]() |
4a703d7cf6 | ||
![]() |
95470a40a7 | ||
![]() |
56d4658b36 | ||
![]() |
f68008b002 | ||
![]() |
6a5ef5b6f2 | ||
![]() |
95b9284e79 | ||
![]() |
8317772436 |
@@ -27,7 +27,12 @@
|
||||
- リアクションの取り消し/変更時に確認ダイアログを出すように
|
||||
- 開発者モードを追加
|
||||
- AiScriptを0.13.3に更新
|
||||
- Deck UIを使用している場合、`/`以外にアクセスした際にZen UIで表示するように
|
||||
- メインカラムを設置していない場合の問題を解決
|
||||
- Fix: URLプレビューで情報が取得できなかった際の挙動を修正
|
||||
- Fix: Safari、Firefoxでの新規登録時、パスワードマネージャーにメールアドレスが登録されていた挙動を修正
|
||||
- fix:ロールタイムラインが無効でも投稿が流れてしまう問題の修正
|
||||
- fix:ロールタイムラインにて全ての投稿が流れてしまう問題の修正
|
||||
|
||||
## 13.12.2
|
||||
|
||||
|
@@ -1,25 +0,0 @@
|
||||
DONATORS
|
||||
========
|
||||
The list of people who have sent donation for Misskey.
|
||||
|
||||
(In random order, honorific titles are omitted.)
|
||||
|
||||
* らふぁ
|
||||
* 俺様
|
||||
* なぎうり
|
||||
* スルメ https://surume.tk/
|
||||
* 藍
|
||||
* 音船 https://otofune.me/
|
||||
* aqz https://misskey.xyz/aqz
|
||||
* kotodu "虚無創作中"
|
||||
* Maya Minatsuki
|
||||
* Knzk https://knzk.me/@Knzk
|
||||
* ねじりわさび https://knzk.me/@y
|
||||
* NCLS https://knzk.me/@imncls]
|
||||
* こじま @skoji@sandbox.skoji.jp
|
||||
|
||||
:heart: Thanks for donating, guys!
|
||||
|
||||
---
|
||||
|
||||
If your name is missing, please contact us!
|
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "misskey",
|
||||
"version": "13.13.0-beta.2",
|
||||
"version": "13.13.0-beta.3",
|
||||
"codename": "nasubi",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/misskey-dev/misskey.git"
|
||||
},
|
||||
"packageManager": "pnpm@8.3.1",
|
||||
"packageManager": "pnpm@8.5.1",
|
||||
"workspaces": [
|
||||
"packages/frontend",
|
||||
"packages/backend",
|
||||
|
@@ -35,25 +35,26 @@
|
||||
"@swc/core-win32-x64-msvc": "1.3.56",
|
||||
"@tensorflow/tfjs": "4.4.0",
|
||||
"@tensorflow/tfjs-node": "4.4.0",
|
||||
"slacc-android-arm-eabi": "0.0.7",
|
||||
"slacc-android-arm64": "0.0.7",
|
||||
"slacc-darwin-arm64": "0.0.7",
|
||||
"slacc-darwin-universal": "0.0.7",
|
||||
"slacc-darwin-x64": "0.0.7",
|
||||
"slacc-linux-arm-gnueabihf": "0.0.7",
|
||||
"slacc-linux-arm64-gnu": "0.0.7",
|
||||
"slacc-linux-arm64-musl": "0.0.7",
|
||||
"slacc-linux-x64-gnu": "0.0.7",
|
||||
"slacc-win32-arm64-msvc": "0.0.7",
|
||||
"slacc-win32-x64-msvc": "0.0.7"
|
||||
"slacc-android-arm-eabi": "0.0.9",
|
||||
"slacc-android-arm64": "0.0.9",
|
||||
"slacc-darwin-arm64": "0.0.9",
|
||||
"slacc-darwin-universal": "0.0.9",
|
||||
"slacc-darwin-x64": "0.0.9",
|
||||
"slacc-freebsd-x64": "0.0.9",
|
||||
"slacc-linux-arm-gnueabihf": "0.0.9",
|
||||
"slacc-linux-arm64-gnu": "0.0.9",
|
||||
"slacc-linux-arm64-musl": "0.0.9",
|
||||
"slacc-linux-x64-gnu": "0.0.9",
|
||||
"slacc-win32-arm64-msvc": "0.0.9",
|
||||
"slacc-win32-x64-msvc": "0.0.9"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "3.321.1",
|
||||
"@aws-sdk/lib-storage": "3.321.1",
|
||||
"@aws-sdk/node-http-handler": "3.321.1",
|
||||
"@bull-board/api": "5.1.2",
|
||||
"@bull-board/fastify": "5.1.2",
|
||||
"@bull-board/ui": "5.1.2",
|
||||
"@bull-board/api": "5.2.0",
|
||||
"@bull-board/fastify": "5.2.0",
|
||||
"@bull-board/ui": "5.2.0",
|
||||
"@discordapp/twemoji": "14.1.2",
|
||||
"@fastify/accepts": "4.1.0",
|
||||
"@fastify/cookie": "8.3.0",
|
||||
@@ -62,13 +63,13 @@
|
||||
"@fastify/multipart": "7.6.0",
|
||||
"@fastify/static": "6.10.1",
|
||||
"@fastify/view": "7.4.1",
|
||||
"@nestjs/common": "9.4.0",
|
||||
"@nestjs/core": "9.4.0",
|
||||
"@nestjs/testing": "9.4.0",
|
||||
"@nestjs/common": "9.4.1",
|
||||
"@nestjs/core": "9.4.1",
|
||||
"@nestjs/testing": "9.4.1",
|
||||
"@peertube/http-signature": "1.7.0",
|
||||
"@sinonjs/fake-timers": "10.0.2",
|
||||
"@swc/cli": "0.1.62",
|
||||
"@swc/core": "1.3.56",
|
||||
"@swc/core": "1.3.59",
|
||||
"accepts": "1.3.8",
|
||||
"ajv": "8.12.0",
|
||||
"archiver": "5.3.1",
|
||||
@@ -93,7 +94,7 @@
|
||||
"fluent-ffmpeg": "2.1.2",
|
||||
"form-data": "4.0.0",
|
||||
"got": "12.6.0",
|
||||
"happy-dom": "9.16.0",
|
||||
"happy-dom": "9.19.2",
|
||||
"hpagent": "1.2.0",
|
||||
"ioredis": "5.3.2",
|
||||
"ip-cidr": "3.1.0",
|
||||
@@ -116,7 +117,7 @@
|
||||
"os-utils": "0.0.14",
|
||||
"otpauth": "9.1.2",
|
||||
"parse5": "7.1.2",
|
||||
"pg": "8.10.0",
|
||||
"pg": "8.11.0",
|
||||
"private-ip": "3.0.0",
|
||||
"probe-image-size": "7.2.3",
|
||||
"promise-limit": "2.7.0",
|
||||
@@ -136,10 +137,10 @@
|
||||
"s-age": "1.1.2",
|
||||
"sanitize-html": "2.10.0",
|
||||
"seedrandom": "3.0.5",
|
||||
"semver": "7.5.0",
|
||||
"semver": "7.5.1",
|
||||
"sharp": "0.32.1",
|
||||
"sharp-read-bmp": "github:misskey-dev/sharp-read-bmp",
|
||||
"slacc": "0.0.7",
|
||||
"slacc": "0.0.9",
|
||||
"strict-event-emitter-types": "2.0.0",
|
||||
"stringz": "2.1.0",
|
||||
"summaly": "github:misskey-dev/summaly",
|
||||
@@ -178,9 +179,9 @@
|
||||
"@types/jsonld": "1.5.8",
|
||||
"@types/jsrsasign": "10.5.8",
|
||||
"@types/mime-types": "2.1.1",
|
||||
"@types/node": "20.1.3",
|
||||
"@types/node": "20.2.1",
|
||||
"@types/node-fetch": "3.0.3",
|
||||
"@types/nodemailer": "6.4.7",
|
||||
"@types/nodemailer": "6.4.8",
|
||||
"@types/oauth": "0.9.1",
|
||||
"@types/pg": "8.6.6",
|
||||
"@types/pug": "2.0.6",
|
||||
|
@@ -306,6 +306,14 @@ export class RoleService implements OnApplicationShutdown {
|
||||
return user.isRoot || (await this.getUserRoles(user.id)).some(r => r.isAdministrator);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async isExplorable(role: { id: Role['id']} | null): Promise<boolean> {
|
||||
if (role == null) return false;
|
||||
const check = await this.rolesRepository.findOneBy({ id: role.id });
|
||||
if (check == null) return false;
|
||||
return check.isExplorable;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async getModeratorIds(includeAdmins = true): Promise<User['id'][]> {
|
||||
const roles = await this.rolesCache.fetch(() => this.rolesRepository.findBy({}));
|
||||
|
@@ -93,6 +93,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
|
||||
const query = this.notesRepository.createQueryBuilder('note')
|
||||
.where('note.id IN (:...noteIds)', { noteIds: noteIds })
|
||||
.andWhere('(note.visibility = \'public\')')
|
||||
.innerJoinAndSelect('note.user', 'user')
|
||||
.leftJoinAndSelect('note.reply', 'reply')
|
||||
.leftJoinAndSelect('note.renote', 'renote')
|
||||
|
@@ -5,15 +5,17 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import Channel from '../channel.js';
|
||||
import { StreamMessages } from '../types.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
|
||||
class RoleTimelineChannel extends Channel {
|
||||
public readonly chName = 'roleTimeline';
|
||||
public static shouldShare = false;
|
||||
public static requireCredential = false;
|
||||
private roleId: string;
|
||||
|
||||
|
||||
constructor(
|
||||
private noteEntityService: NoteEntityService,
|
||||
private roleservice: RoleService,
|
||||
|
||||
id: string,
|
||||
connection: Channel['connection'],
|
||||
@@ -34,6 +36,11 @@ class RoleTimelineChannel extends Channel {
|
||||
if (data.type === 'note') {
|
||||
const note = data.body;
|
||||
|
||||
if (!(await this.roleservice.isExplorable({ id: this.roleId }))) {
|
||||
return;
|
||||
}
|
||||
if (note.visibility !== 'public') return;
|
||||
|
||||
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
|
||||
if (isUserRelated(note, this.userIdsWhoMeMuting)) return;
|
||||
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
|
||||
@@ -61,6 +68,7 @@ export class RoleTimelineChannelService {
|
||||
|
||||
constructor(
|
||||
private noteEntityService: NoteEntityService,
|
||||
private roleservice: RoleService,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -68,6 +76,7 @@ export class RoleTimelineChannelService {
|
||||
public create(id: string, connection: Channel['connection']): RoleTimelineChannel {
|
||||
return new RoleTimelineChannel(
|
||||
this.noteEntityService,
|
||||
this.roleservice,
|
||||
id,
|
||||
connection,
|
||||
);
|
||||
|
@@ -25,7 +25,6 @@ html
|
||||
meta(name='referrer' content='origin')
|
||||
meta(name='theme-color' content= themeColor || '#86b300')
|
||||
meta(name='theme-color-orig' content= themeColor || '#86b300')
|
||||
meta(property='twitter:card' content='summary')
|
||||
meta(property='og:site_name' content= instanceName || 'Misskey')
|
||||
meta(name='viewport' content='width=device-width, initial-scale=1')
|
||||
link(rel='icon' href= icon || '/favicon.ico')
|
||||
@@ -59,6 +58,7 @@ html
|
||||
meta(property='og:title' content= title || 'Misskey')
|
||||
meta(property='og:description' content= desc || '✨🌎✨ A interplanetary communication platform ✨🚀✨')
|
||||
meta(property='og:image' content= img)
|
||||
meta(property='twitter:card' content='summary')
|
||||
|
||||
style
|
||||
include ../style.css
|
||||
|
@@ -16,3 +16,4 @@ block og
|
||||
meta(property='og:description' content= channel.description)
|
||||
meta(property='og:url' content= url)
|
||||
meta(property='og:image' content= channel.bannerUrl)
|
||||
meta(property='twitter:card' content='summary')
|
||||
|
@@ -17,6 +17,7 @@ block og
|
||||
meta(property='og:description' content= clip.description)
|
||||
meta(property='og:url' content= url)
|
||||
meta(property='og:image' content= avatarUrl)
|
||||
meta(property='twitter:card' content='summary')
|
||||
|
||||
block meta
|
||||
if profile.noCrawle
|
||||
|
@@ -17,6 +17,7 @@ block og
|
||||
meta(property='og:description' content= flash.summary)
|
||||
meta(property='og:url' content= url)
|
||||
meta(property='og:image' content= avatarUrl)
|
||||
meta(property='twitter:card' content='summary')
|
||||
|
||||
block meta
|
||||
if profile.noCrawle
|
||||
|
@@ -17,6 +17,7 @@ block og
|
||||
meta(property='og:description' content= post.description)
|
||||
meta(property='og:url' content= url)
|
||||
meta(property='og:image' content= post.files[0].thumbnailUrl)
|
||||
meta(property='twitter:card' content='summary_large_image')
|
||||
|
||||
block meta
|
||||
if user.host || profile.noCrawle
|
||||
|
@@ -5,6 +5,8 @@ block vars
|
||||
- const title = user.name ? `${user.name} (@${user.username})` : `@${user.username}`;
|
||||
- const url = `${config.url}/notes/${note.id}`;
|
||||
- const isRenote = note.renote && note.text == null && note.fileIds.length == 0 && note.poll == null;
|
||||
- const image = (note.files || []).find(file => file.type.startsWith('image/') && !file.type.isSensitive)
|
||||
- const video = (note.files || []).find(file => file.type.startsWith('video/') && !file.type.isSensitive)
|
||||
|
||||
block title
|
||||
= `${title} | ${instanceName}`
|
||||
@@ -17,7 +19,19 @@ block og
|
||||
meta(property='og:title' content= title)
|
||||
meta(property='og:description' content= summary)
|
||||
meta(property='og:url' content= url)
|
||||
meta(property='og:image' content= avatarUrl)
|
||||
if video
|
||||
meta(property='og:video:url' content= video.url)
|
||||
meta(property='og:video:secure_url' content= video.url)
|
||||
meta(property='og:video:type' content= video.type)
|
||||
// FIXME: add width and height
|
||||
// FIXME: add embed player for Twitter
|
||||
if image
|
||||
meta(property='twitter:card' content='summary_large_image')
|
||||
meta(property='og:image' content= image.url)
|
||||
else
|
||||
meta(property='twitter:card' content='summary')
|
||||
meta(property='og:image' content= avatarUrl)
|
||||
|
||||
|
||||
block meta
|
||||
if user.host || isRenote || profile.noCrawle
|
||||
|
@@ -17,6 +17,7 @@ block og
|
||||
meta(property='og:description' content= page.summary)
|
||||
meta(property='og:url' content= url)
|
||||
meta(property='og:image' content= page.eyeCatchingImage ? page.eyeCatchingImage.thumbnailUrl : avatarUrl)
|
||||
meta(property='twitter:card' content= page.eyeCatchingImage ? 'summary_large_image' : 'summary')
|
||||
|
||||
block meta
|
||||
if profile.noCrawle
|
||||
|
@@ -16,6 +16,7 @@ block og
|
||||
meta(property='og:description' content= profile.description)
|
||||
meta(property='og:url' content= url)
|
||||
meta(property='og:image' content= avatarUrl)
|
||||
meta(property='twitter:card' content='summary')
|
||||
|
||||
block meta
|
||||
if user.host || profile.noCrawle
|
||||
|
653
packages/backend/test/e2e/antennas.ts
Normal file
653
packages/backend/test/e2e/antennas.ts
Normal file
@@ -0,0 +1,653 @@
|
||||
process.env.NODE_ENV = 'test';
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { inspect } from 'node:util';
|
||||
import { DEFAULT_POLICIES } from '@/core/RoleService.js';
|
||||
import type { Packed } from '@/misc/json-schema.js';
|
||||
import {
|
||||
signup,
|
||||
post,
|
||||
userList,
|
||||
page,
|
||||
role,
|
||||
startServer,
|
||||
api,
|
||||
successfulApiCall,
|
||||
failedApiCall,
|
||||
uploadFile,
|
||||
testPaginationConsistency,
|
||||
} from '../utils.js';
|
||||
import type * as misskey from 'misskey-js';
|
||||
import type { INestApplicationContext } from '@nestjs/common';
|
||||
|
||||
const compareBy = <T extends { id: string }>(selector: (s: T) => string = (s: T): string => s.id) => (a: T, b: T): number => {
|
||||
return selector(a).localeCompare(selector(b));
|
||||
};
|
||||
|
||||
describe('アンテナ', () => {
|
||||
// エンティティとしてのアンテナを主眼においたテストを記述する
|
||||
// (Antennaを返すエンドポイント、Antennaエンティティを書き換えるエンドポイント、Antennaからノートを取得するエンドポイントをテストする)
|
||||
|
||||
// BUG misskey-jsとjson-schemaが一致していない。
|
||||
// - srcのenumにgroupが残っている
|
||||
// - userGroupIdが残っている, isActiveがない
|
||||
type Antenna = misskey.entities.Antenna | Packed<'Antenna'>;
|
||||
type User = misskey.entities.MeDetailed & { token: string };
|
||||
type Note = misskey.entities.Note;
|
||||
|
||||
// アンテナを作成できる最小のパラメタ
|
||||
const defaultParam = {
|
||||
caseSensitive: false,
|
||||
excludeKeywords: [['']],
|
||||
keywords: [['keyword']],
|
||||
name: 'test',
|
||||
notify: false,
|
||||
src: 'all' as const,
|
||||
userListId: null,
|
||||
users: [''],
|
||||
withFile: false,
|
||||
withReplies: false,
|
||||
};
|
||||
|
||||
let app: INestApplicationContext;
|
||||
|
||||
let root: User;
|
||||
let alice: User;
|
||||
let bob: User;
|
||||
let carol: User;
|
||||
|
||||
let alicePost: Note;
|
||||
let aliceList: misskey.entities.UserList;
|
||||
let bobFile: misskey.entities.DriveFile;
|
||||
let bobList: misskey.entities.UserList;
|
||||
|
||||
let userNotExplorable: User;
|
||||
let userLocking: User;
|
||||
let userSilenced: User;
|
||||
let userSuspended: User;
|
||||
let userDeletedBySelf: User;
|
||||
let userDeletedByAdmin: User;
|
||||
let userFollowingAlice: User;
|
||||
let userFollowedByAlice: User;
|
||||
let userBlockingAlice: User;
|
||||
let userBlockedByAlice: User;
|
||||
let userMutingAlice: User;
|
||||
let userMutedByAlice: User;
|
||||
|
||||
beforeAll(async () => {
|
||||
app = await startServer();
|
||||
}, 1000 * 60 * 2);
|
||||
|
||||
beforeAll(async () => {
|
||||
root = await signup({ username: 'root' });
|
||||
alice = await signup({ username: 'alice' });
|
||||
alicePost = await post(alice, { text: 'test' });
|
||||
aliceList = await userList(alice, {});
|
||||
bob = await signup({ username: 'bob' });
|
||||
aliceList = await userList(alice, {});
|
||||
bobFile = (await uploadFile(bob)).body;
|
||||
bobList = await userList(bob);
|
||||
carol = await signup({ username: 'carol' });
|
||||
await api('users/lists/push', { listId: aliceList.id, userId: bob.id }, alice);
|
||||
await api('users/lists/push', { listId: aliceList.id, userId: carol.id }, alice);
|
||||
|
||||
userNotExplorable = await signup({ username: 'userNotExplorable' });
|
||||
await post(userNotExplorable, { text: 'test' });
|
||||
await api('i/update', { isExplorable: false }, userNotExplorable);
|
||||
userLocking = await signup({ username: 'userLocking' });
|
||||
await post(userLocking, { text: 'test' });
|
||||
await api('i/update', { isLocked: true }, userLocking);
|
||||
userSilenced = await signup({ username: 'userSilenced' });
|
||||
await post(userSilenced, { text: 'test' });
|
||||
const roleSilenced = await role(root, {}, { canPublicNote: { priority: 0, useDefault: false, value: false } });
|
||||
await api('admin/roles/assign', { userId: userSilenced.id, roleId: roleSilenced.id }, root);
|
||||
userSuspended = await signup({ username: 'userSuspended' });
|
||||
await post(userSuspended, { text: 'test' });
|
||||
await successfulApiCall({ endpoint: 'i/update', parameters: { description: '#user_testuserSuspended' }, user: userSuspended });
|
||||
await api('admin/suspend-user', { userId: userSuspended.id }, root);
|
||||
userDeletedBySelf = await signup({ username: 'userDeletedBySelf', password: 'userDeletedBySelf' });
|
||||
await post(userDeletedBySelf, { text: 'test' });
|
||||
await api('i/delete-account', { password: 'userDeletedBySelf' }, userDeletedBySelf);
|
||||
userDeletedByAdmin = await signup({ username: 'userDeletedByAdmin' });
|
||||
await post(userDeletedByAdmin, { text: 'test' });
|
||||
await api('admin/delete-account', { userId: userDeletedByAdmin.id }, root);
|
||||
userFollowedByAlice = await signup({ username: 'userFollowedByAlice' });
|
||||
await post(userFollowedByAlice, { text: 'test' });
|
||||
await api('following/create', { userId: userFollowedByAlice.id }, alice);
|
||||
userFollowingAlice = await signup({ username: 'userFollowingAlice' });
|
||||
await post(userFollowingAlice, { text: 'test' });
|
||||
await api('following/create', { userId: alice.id }, userFollowingAlice);
|
||||
userBlockingAlice = await signup({ username: 'userBlockingAlice' });
|
||||
await post(userBlockingAlice, { text: 'test' });
|
||||
await api('blocking/create', { userId: alice.id }, userBlockingAlice);
|
||||
userBlockedByAlice = await signup({ username: 'userBlockedByAlice' });
|
||||
await post(userBlockedByAlice, { text: 'test' });
|
||||
await api('blocking/create', { userId: userBlockedByAlice.id }, alice);
|
||||
userMutingAlice = await signup({ username: 'userMutingAlice' });
|
||||
await post(userMutingAlice, { text: 'test' });
|
||||
await api('mute/create', { userId: alice.id }, userMutingAlice);
|
||||
userMutedByAlice = await signup({ username: 'userMutedByAlice' });
|
||||
await post(userMutedByAlice, { text: 'test' });
|
||||
await api('mute/create', { userId: userMutedByAlice.id }, alice);
|
||||
}, 1000 * 60 * 10);
|
||||
|
||||
afterAll(async () => {
|
||||
await app.close();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
// テスト間で影響し合わないように毎回全部消す。
|
||||
for (const user of [alice, bob]) {
|
||||
const list = await api('/antennas/list', {}, user);
|
||||
for (const antenna of list.body) {
|
||||
await api('/antennas/delete', { antennaId: antenna.id }, user);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
//#region 作成(antennas/create)
|
||||
|
||||
test('が作成できること、キーが過不足なく入っていること。', async () => {
|
||||
const response = await successfulApiCall({
|
||||
endpoint: 'antennas/create',
|
||||
parameters: { ...defaultParam },
|
||||
user: alice,
|
||||
});
|
||||
assert.match(response.id, /[0-9a-z]{10}/);
|
||||
const expected = {
|
||||
id: response.id,
|
||||
caseSensitive: false,
|
||||
createdAt: new Date(response.createdAt).toISOString(),
|
||||
excludeKeywords: [['']],
|
||||
hasUnreadNote: false,
|
||||
isActive: true,
|
||||
keywords: [['keyword']],
|
||||
name: 'test',
|
||||
notify: false,
|
||||
src: 'all',
|
||||
userListId: null,
|
||||
users: [''],
|
||||
withFile: false,
|
||||
withReplies: false,
|
||||
} as Antenna;
|
||||
assert.deepStrictEqual(response, expected);
|
||||
});
|
||||
|
||||
test('が上限いっぱいまで作成できること', async () => {
|
||||
// antennaLimit + 1まで作れるのがキモ
|
||||
const response = await Promise.all([...Array(DEFAULT_POLICIES.antennaLimit + 1)].map(() => successfulApiCall({
|
||||
endpoint: 'antennas/create',
|
||||
parameters: { ...defaultParam },
|
||||
user: alice,
|
||||
})));
|
||||
|
||||
const expected = await successfulApiCall({ endpoint: 'antennas/list', parameters: {}, user: alice });
|
||||
assert.deepStrictEqual(
|
||||
response.sort(compareBy(s => s.id)),
|
||||
expected.sort(compareBy(s => s.id)));
|
||||
|
||||
failedApiCall({
|
||||
endpoint: 'antennas/create',
|
||||
parameters: { ...defaultParam },
|
||||
user: alice,
|
||||
}, {
|
||||
status: 400,
|
||||
code: 'TOO_MANY_ANTENNAS',
|
||||
id: 'faf47050-e8b5-438c-913c-db2b1576fde4',
|
||||
});
|
||||
});
|
||||
|
||||
test('を作成するとき他人のリストを指定したらエラーになる', async () => {
|
||||
failedApiCall({
|
||||
endpoint: 'antennas/create',
|
||||
parameters: { ...defaultParam, src: 'list', userListId: bobList.id },
|
||||
user: alice,
|
||||
}, {
|
||||
status: 400,
|
||||
code: 'NO_SUCH_USER_LIST',
|
||||
id: '95063e93-a283-4b8b-9aa5-bcdb8df69a7f',
|
||||
});
|
||||
});
|
||||
|
||||
const antennaParamPattern = [
|
||||
{ parameters: (): object => ({ name: 'x'.repeat(100) }) },
|
||||
{ parameters: (): object => ({ name: 'x' }) },
|
||||
{ parameters: (): object => ({ src: 'home' }) },
|
||||
{ parameters: (): object => ({ src: 'all' }) },
|
||||
{ parameters: (): object => ({ src: 'users' }) },
|
||||
{ parameters: (): object => ({ src: 'list' }) },
|
||||
{ parameters: (): object => ({ userListId: null }) },
|
||||
{ parameters: (): object => ({ src: 'list', userListId: aliceList.id }) },
|
||||
{ parameters: (): object => ({ keywords: [['x']] }) },
|
||||
{ parameters: (): object => ({ keywords: [['a', 'b', 'c'], ['x'], ['y'], ['z']] }) },
|
||||
{ parameters: (): object => ({ excludeKeywords: [['a', 'b', 'c'], ['x'], ['y'], ['z']] }) },
|
||||
{ parameters: (): object => ({ users: [alice.username] }) },
|
||||
{ parameters: (): object => ({ users: [alice.username, bob.username, carol.username] }) },
|
||||
{ parameters: (): object => ({ caseSensitive: false }) },
|
||||
{ parameters: (): object => ({ caseSensitive: true }) },
|
||||
{ parameters: (): object => ({ withReplies: false }) },
|
||||
{ parameters: (): object => ({ withReplies: true }) },
|
||||
{ parameters: (): object => ({ withFile: false }) },
|
||||
{ parameters: (): object => ({ withFile: true }) },
|
||||
{ parameters: (): object => ({ notify: false }) },
|
||||
{ parameters: (): object => ({ notify: true }) },
|
||||
];
|
||||
test.each(antennaParamPattern)('を作成できること($#)', async ({ parameters }) => {
|
||||
const response = await successfulApiCall({
|
||||
endpoint: 'antennas/create',
|
||||
parameters: { ...defaultParam, ...parameters() },
|
||||
user: alice,
|
||||
});
|
||||
const expected = { ...response, ...parameters() };
|
||||
assert.deepStrictEqual(response, expected);
|
||||
});
|
||||
|
||||
//#endregion
|
||||
//#region 更新(antennas/update)
|
||||
|
||||
test.each(antennaParamPattern)('を変更できること($#)', async ({ parameters }) => {
|
||||
const antenna = await successfulApiCall({ endpoint: 'antennas/create', parameters: defaultParam, user: alice });
|
||||
const response = await successfulApiCall({
|
||||
endpoint: 'antennas/update',
|
||||
parameters: { antennaId: antenna.id, ...defaultParam, ...parameters() },
|
||||
user: alice,
|
||||
});
|
||||
const expected = { ...response, ...parameters() };
|
||||
assert.deepStrictEqual(response, expected);
|
||||
});
|
||||
test.todo('は他人のものは変更できない');
|
||||
|
||||
test('を変更するとき他人のリストを指定したらエラーになる', async () => {
|
||||
const antenna = await successfulApiCall({ endpoint: 'antennas/create', parameters: defaultParam, user: alice });
|
||||
failedApiCall({
|
||||
endpoint: 'antennas/update',
|
||||
parameters: { antennaId: antenna.id, ...defaultParam, src: 'list', userListId: bobList.id },
|
||||
user: alice,
|
||||
}, {
|
||||
status: 400,
|
||||
code: 'NO_SUCH_USER_LIST',
|
||||
id: '1c6b35c9-943e-48c2-81e4-2844989407f7',
|
||||
});
|
||||
});
|
||||
|
||||
//#endregion
|
||||
//#region 表示(antennas/show)
|
||||
|
||||
test('をID指定で表示できること。', async () => {
|
||||
const antenna = await successfulApiCall({ endpoint: 'antennas/create', parameters: defaultParam, user: alice });
|
||||
const response = await successfulApiCall({
|
||||
endpoint: 'antennas/show',
|
||||
parameters: { antennaId: antenna.id },
|
||||
user: alice,
|
||||
});
|
||||
const expected = { ...antenna };
|
||||
assert.deepStrictEqual(response, expected);
|
||||
});
|
||||
test.todo('は他人のものをID指定で表示できない');
|
||||
|
||||
//#endregion
|
||||
//#region 一覧(antennas/list)
|
||||
|
||||
test('をリスト形式で取得できること。', async () => {
|
||||
const antenna = await successfulApiCall({ endpoint: 'antennas/create', parameters: defaultParam, user: alice });
|
||||
await successfulApiCall({ endpoint: 'antennas/create', parameters: defaultParam, user: bob });
|
||||
const response = await successfulApiCall({
|
||||
endpoint: 'antennas/list',
|
||||
parameters: {},
|
||||
user: alice,
|
||||
});
|
||||
const expected = [{ ...antenna }];
|
||||
assert.deepStrictEqual(response, expected);
|
||||
});
|
||||
|
||||
//#endregion
|
||||
//#region 削除(antennas/delete)
|
||||
|
||||
test('を削除できること。', async () => {
|
||||
const antenna = await successfulApiCall({ endpoint: 'antennas/create', parameters: defaultParam, user: alice });
|
||||
const response = await successfulApiCall({
|
||||
endpoint: 'antennas/delete',
|
||||
parameters: { antennaId: antenna.id },
|
||||
user: alice,
|
||||
});
|
||||
assert.deepStrictEqual(response, null);
|
||||
const list = await successfulApiCall({ endpoint: 'antennas/list', parameters: {}, user: alice });
|
||||
assert.deepStrictEqual(list, []);
|
||||
});
|
||||
test.todo('は他人のものを削除できない');
|
||||
|
||||
//#endregion
|
||||
|
||||
describe('のノート', () => {
|
||||
//#region アンテナのノート取得(antennas/notes)
|
||||
|
||||
test('を取得できること。', async () => {
|
||||
const keyword = 'キーワード';
|
||||
await post(bob, { text: `test ${keyword} beforehand` });
|
||||
const antenna = await successfulApiCall({
|
||||
endpoint: 'antennas/create',
|
||||
parameters: { ...defaultParam, keywords: [[keyword]] },
|
||||
user: alice,
|
||||
});
|
||||
const note = await post(bob, { text: `test ${keyword}` });
|
||||
const response = await successfulApiCall({
|
||||
endpoint: 'antennas/notes',
|
||||
parameters: { antennaId: antenna.id },
|
||||
user: alice,
|
||||
});
|
||||
const expected = [note];
|
||||
assert.deepStrictEqual(response, expected);
|
||||
});
|
||||
|
||||
const keyword = 'キーワード';
|
||||
test.each([
|
||||
{
|
||||
label: '全体から',
|
||||
parameters: (): object => ({ src: 'all' }),
|
||||
posts: [
|
||||
{ note: (): Promise<Note> => post(alice, { text: `${keyword}` }), included: true },
|
||||
{ note: (): Promise<Note> => post(userFollowedByAlice, { text: `${keyword}` }), included: true },
|
||||
{ note: (): Promise<Note> => post(bob, { text: `test ${keyword}` }), included: true },
|
||||
{ note: (): Promise<Note> => post(carol, { text: `test ${keyword}` }), included: true },
|
||||
],
|
||||
},
|
||||
{
|
||||
// BUG e4144a1 以降home指定は壊れている(allと同じ)
|
||||
label: 'ホーム指定はallと同じ',
|
||||
parameters: (): object => ({ src: 'home' }),
|
||||
posts: [
|
||||
{ note: (): Promise<Note> => post(alice, { text: `${keyword}` }), included: true },
|
||||
{ note: (): Promise<Note> => post(userFollowedByAlice, { text: `${keyword}` }), included: true },
|
||||
{ note: (): Promise<Note> => post(bob, { text: `test ${keyword}` }), included: true },
|
||||
{ note: (): Promise<Note> => post(carol, { text: `test ${keyword}` }), included: true },
|
||||
],
|
||||
},
|
||||
{
|
||||
// https://github.com/misskey-dev/misskey/issues/9025
|
||||
label: 'ただし、フォロワー限定投稿とDM投稿を含まない。フォロワーであっても。',
|
||||
parameters: (): object => ({}),
|
||||
posts: [
|
||||
{ note: (): Promise<Note> => post(userFollowedByAlice, { text: `${keyword}`, visibility: 'public' }), included: true },
|
||||
{ note: (): Promise<Note> => post(userFollowedByAlice, { text: `${keyword}`, visibility: 'home' }), included: true },
|
||||
{ note: (): Promise<Note> => post(userFollowedByAlice, { text: `${keyword}`, visibility: 'followers' }) },
|
||||
{ note: (): Promise<Note> => post(userFollowedByAlice, { text: `${keyword}`, visibility: 'specified', visibleUserIds: [alice.id] }) },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'ブロックしているユーザーのノートは含む',
|
||||
parameters: (): object => ({}),
|
||||
posts: [
|
||||
{ note: (): Promise<Note> => post(userBlockedByAlice, { text: `${keyword}` }), included: true },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'ブロックされているユーザーのノートは含まない',
|
||||
parameters: (): object => ({}),
|
||||
posts: [
|
||||
{ note: (): Promise<Note> => post(userBlockingAlice, { text: `${keyword}` }) },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'ミュートしているユーザーのノートは含まない',
|
||||
parameters: (): object => ({}),
|
||||
posts: [
|
||||
{ note: (): Promise<Note> => post(userMutedByAlice, { text: `${keyword}` }) },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'ミュートされているユーザーのノートは含む',
|
||||
parameters: (): object => ({}),
|
||||
posts: [
|
||||
{ note: (): Promise<Note> => post(userMutingAlice, { text: `${keyword}` }), included: true },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '「見つけやすくする」がOFFのユーザーのノートも含まれる',
|
||||
parameters: (): object => ({}),
|
||||
posts: [
|
||||
{ note: (): Promise<Note> => post(userNotExplorable, { text: `${keyword}` }), included: true },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '鍵付きユーザーのノートも含まれる',
|
||||
parameters: (): object => ({}),
|
||||
posts: [
|
||||
{ note: (): Promise<Note> => post(userLocking, { text: `${keyword}` }), included: true },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'サイレンスのノートも含まれる',
|
||||
parameters: (): object => ({}),
|
||||
posts: [
|
||||
{ note: (): Promise<Note> => post(userSilenced, { text: `${keyword}` }), included: true },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '削除ユーザーのノートも含まれる',
|
||||
parameters: (): object => ({}),
|
||||
posts: [
|
||||
{ note: (): Promise<Note> => post(userDeletedBySelf, { text: `${keyword}` }), included: true },
|
||||
{ note: (): Promise<Note> => post(userDeletedByAdmin, { text: `${keyword}` }), included: true },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'ユーザー指定で',
|
||||
parameters: (): object => ({ src: 'users', users: [`@${bob.username}`, `@${carol.username}`] }),
|
||||
posts: [
|
||||
{ note: (): Promise<Note> => post(alice, { text: `test ${keyword}` }) },
|
||||
{ note: (): Promise<Note> => post(bob, { text: `test ${keyword}` }), included: true },
|
||||
{ note: (): Promise<Note> => post(carol, { text: `test ${keyword}` }), included: true },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'リスト指定で',
|
||||
parameters: (): object => ({ src: 'list', userListId: aliceList.id }),
|
||||
posts: [
|
||||
{ note: (): Promise<Note> => post(alice, { text: `test ${keyword}` }) },
|
||||
{ note: (): Promise<Note> => post(bob, { text: `test ${keyword}` }), included: true },
|
||||
{ note: (): Promise<Note> => post(carol, { text: `test ${keyword}` }), included: true },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'CWにもマッチする',
|
||||
parameters: (): object => ({ keywords: [[keyword]] }),
|
||||
posts: [
|
||||
{ note: (): Promise<Note> => post(bob, { text: 'test', cw: `cw ${keyword}` }), included: true },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'キーワード1つ',
|
||||
parameters: (): object => ({ keywords: [[keyword]] }),
|
||||
posts: [
|
||||
{ note: (): Promise<Note> => post(alice, { text: 'test' }) },
|
||||
{ note: (): Promise<Note> => post(bob, { text: `test ${keyword}` }), included: true },
|
||||
{ note: (): Promise<Note> => post(carol, { text: 'test' }) },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'キーワード3つ(AND)',
|
||||
parameters: (): object => ({ keywords: [['A', 'B', 'C']] }),
|
||||
posts: [
|
||||
{ note: (): Promise<Note> => post(bob, { text: 'test A' }) },
|
||||
{ note: (): Promise<Note> => post(bob, { text: 'test A B' }) },
|
||||
{ note: (): Promise<Note> => post(bob, { text: 'test B C' }) },
|
||||
{ note: (): Promise<Note> => post(bob, { text: 'test A B C' }), included: true },
|
||||
{ note: (): Promise<Note> => post(bob, { text: 'test C B A A B C' }), included: true },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'キーワード3つ(OR)',
|
||||
parameters: (): object => ({ keywords: [['A'], ['B'], ['C']] }),
|
||||
posts: [
|
||||
{ note: (): Promise<Note> => post(bob, { text: 'test' }) },
|
||||
{ note: (): Promise<Note> => post(bob, { text: 'test A' }), included: true },
|
||||
{ note: (): Promise<Note> => post(bob, { text: 'test A B' }), included: true },
|
||||
{ note: (): Promise<Note> => post(bob, { text: 'test B C' }), included: true },
|
||||
{ note: (): Promise<Note> => post(bob, { text: 'test B C A' }), included: true },
|
||||
{ note: (): Promise<Note> => post(bob, { text: 'test C B' }), included: true },
|
||||
{ note: (): Promise<Note> => post(bob, { text: 'test C' }), included: true },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '除外ワード3つ(AND)',
|
||||
parameters: (): object => ({ excludeKeywords: [['A', 'B', 'C']] }),
|
||||
posts: [
|
||||
{ note: (): Promise<Note> => post(bob, { text: `test ${keyword}` }), included: true },
|
||||
{ note: (): Promise<Note> => post(bob, { text: `test ${keyword} A` }), included: true },
|
||||
{ note: (): Promise<Note> => post(bob, { text: `test ${keyword} A B` }), included: true },
|
||||
{ note: (): Promise<Note> => post(bob, { text: `test ${keyword} B C` }), included: true },
|
||||
{ note: (): Promise<Note> => post(bob, { text: `test ${keyword} B C A` }) },
|
||||
{ note: (): Promise<Note> => post(bob, { text: `test ${keyword} C B` }), included: true },
|
||||
{ note: (): Promise<Note> => post(bob, { text: `test ${keyword} C` }), included: true },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '除外ワード3つ(OR)',
|
||||
parameters: (): object => ({ excludeKeywords: [['A'], ['B'], ['C']] }),
|
||||
posts: [
|
||||
{ note: (): Promise<Note> => post(bob, { text: `test ${keyword}` }), included: true },
|
||||
{ note: (): Promise<Note> => post(bob, { text: `test ${keyword} A` }) },
|
||||
{ note: (): Promise<Note> => post(bob, { text: `test ${keyword} A B` }) },
|
||||
{ note: (): Promise<Note> => post(bob, { text: `test ${keyword} B C` }) },
|
||||
{ note: (): Promise<Note> => post(bob, { text: `test ${keyword} B C A` }) },
|
||||
{ note: (): Promise<Note> => post(bob, { text: `test ${keyword} C B` }) },
|
||||
{ note: (): Promise<Note> => post(bob, { text: `test ${keyword} C` }) },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'キーワード1つ(大文字小文字区別する)',
|
||||
parameters: (): object => ({ keywords: [['KEYWORD']], caseSensitive: true }),
|
||||
posts: [
|
||||
{ note: (): Promise<Note> => post(bob, { text: 'keyword' }) },
|
||||
{ note: (): Promise<Note> => post(bob, { text: 'kEyWoRd' }) },
|
||||
{ note: (): Promise<Note> => post(bob, { text: 'KEYWORD' }), included: true },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'キーワード1つ(大文字小文字区別しない)',
|
||||
parameters: (): object => ({ keywords: [['KEYWORD']], caseSensitive: false }),
|
||||
posts: [
|
||||
{ note: (): Promise<Note> => post(bob, { text: 'keyword' }), included: true },
|
||||
{ note: (): Promise<Note> => post(bob, { text: 'kEyWoRd' }), included: true },
|
||||
{ note: (): Promise<Note> => post(bob, { text: 'KEYWORD' }), included: true },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '除外ワード1つ(大文字小文字区別する)',
|
||||
parameters: (): object => ({ excludeKeywords: [['KEYWORD']], caseSensitive: true }),
|
||||
posts: [
|
||||
{ note: (): Promise<Note> => post(bob, { text: `${keyword}` }), included: true },
|
||||
{ note: (): Promise<Note> => post(bob, { text: `${keyword} keyword` }), included: true },
|
||||
{ note: (): Promise<Note> => post(bob, { text: `${keyword} kEyWoRd` }), included: true },
|
||||
{ note: (): Promise<Note> => post(bob, { text: `${keyword} KEYWORD` }) },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '除外ワード1つ(大文字小文字区別しない)',
|
||||
parameters: (): object => ({ excludeKeywords: [['KEYWORD']], caseSensitive: false }),
|
||||
posts: [
|
||||
{ note: (): Promise<Note> => post(bob, { text: `${keyword}` }), included: true },
|
||||
{ note: (): Promise<Note> => post(bob, { text: `${keyword} keyword` }) },
|
||||
{ note: (): Promise<Note> => post(bob, { text: `${keyword} kEyWoRd` }) },
|
||||
{ note: (): Promise<Note> => post(bob, { text: `${keyword} KEYWORD` }) },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '添付ファイルを問わない',
|
||||
parameters: (): object => ({ withFile: false }),
|
||||
posts: [
|
||||
{ note: (): Promise<Note> => post(bob, { text: `${keyword}`, fileIds: [bobFile.id] }), included: true },
|
||||
{ note: (): Promise<Note> => post(bob, { text: `${keyword}` }), included: true },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '添付ファイル付きのみ',
|
||||
parameters: (): object => ({ withFile: true }),
|
||||
posts: [
|
||||
{ note: (): Promise<Note> => post(bob, { text: `${keyword}`, fileIds: [bobFile.id] }), included: true },
|
||||
{ note: (): Promise<Note> => post(bob, { text: `${keyword}` }) },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'リプライ以外',
|
||||
parameters: (): object => ({ withReplies: false }),
|
||||
posts: [
|
||||
{ note: (): Promise<Note> => post(bob, { text: `${keyword}`, replyId: alicePost.id }) },
|
||||
{ note: (): Promise<Note> => post(bob, { text: `${keyword}` }), included: true },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'リプライも含む',
|
||||
parameters: (): object => ({ withReplies: true }),
|
||||
posts: [
|
||||
{ note: (): Promise<Note> => post(bob, { text: `${keyword}`, replyId: alicePost.id }), included: true },
|
||||
{ note: (): Promise<Note> => post(bob, { text: `${keyword}` }), included: true },
|
||||
],
|
||||
},
|
||||
])('が取得できること($label)', async ({ parameters, posts }) => {
|
||||
const antenna = await successfulApiCall({
|
||||
endpoint: 'antennas/create',
|
||||
parameters: { ...defaultParam, keywords: [[keyword]], ...parameters() },
|
||||
user: alice,
|
||||
});
|
||||
|
||||
const notes = await posts.reduce(async (prev, current) => {
|
||||
// includedに関わらずnote()は評価して投稿する。
|
||||
const p = await prev;
|
||||
const n = await current.note();
|
||||
if (current.included) return p.concat(n);
|
||||
return p;
|
||||
}, Promise.resolve([] as Note[]));
|
||||
|
||||
// alice視点でNoteを取り直す
|
||||
const expected = await Promise.all(notes.reverse().map(s => successfulApiCall({
|
||||
endpoint: 'notes/show',
|
||||
parameters: { noteId: s.id },
|
||||
user: alice,
|
||||
})));
|
||||
|
||||
const response = await successfulApiCall({
|
||||
endpoint: 'antennas/notes',
|
||||
parameters: { antennaId: antenna.id },
|
||||
user: alice,
|
||||
});
|
||||
assert.deepStrictEqual(
|
||||
response.map(({ userId, id, text }) => ({ userId, id, text })),
|
||||
expected.map(({ userId, id, text }) => ({ userId, id, text })));
|
||||
assert.deepStrictEqual(response, expected);
|
||||
});
|
||||
|
||||
test.skip('が取得でき、日付指定のPaginationに一貫性があること', async () => { });
|
||||
test.each([
|
||||
{ label: 'ID指定', offsetBy: 'id' },
|
||||
|
||||
// BUG sinceDate, untilDateはsinceIdや他のエンドポイントとは異なり、その時刻に一致するレコードを含んでしまう。
|
||||
// { label: '日付指定', offsetBy: 'createdAt' },
|
||||
] as const)('が取得でき、$labelのPaginationに一貫性があること', async ({ offsetBy }) => {
|
||||
const antenna = await successfulApiCall({
|
||||
endpoint: 'antennas/create',
|
||||
parameters: { ...defaultParam, keywords: [[keyword]] },
|
||||
user: alice,
|
||||
});
|
||||
const notes = await [...Array(30)].reduce(async (prev, current, index) => {
|
||||
const p = await prev;
|
||||
const n = await post(alice, { text: `${keyword} (${index})` });
|
||||
return [n].concat(p);
|
||||
}, Promise.resolve([] as Note[]));
|
||||
|
||||
// antennas/notesは降順のみで、昇順をサポートしない。
|
||||
await testPaginationConsistency(notes, async (paginationParam) => {
|
||||
return successfulApiCall({
|
||||
endpoint: 'antennas/notes',
|
||||
parameters: { antennaId: antenna.id, ...paginationParam },
|
||||
user: alice,
|
||||
}) as any as Note[];
|
||||
}, offsetBy, 'desc');
|
||||
});
|
||||
|
||||
// BUG 7日過ぎると作り直すしかない。 https://github.com/misskey-dev/misskey/issues/10476
|
||||
test.todo('を取得したときActiveに戻る');
|
||||
|
||||
//#endregion
|
||||
});
|
||||
});
|
@@ -124,6 +124,13 @@ export const react = async (user: any, note: any, reaction: string): Promise<any
|
||||
}, user);
|
||||
};
|
||||
|
||||
export const userList = async (user: any, userList: any = {}): Promise<any> => {
|
||||
const res = await api('users/lists/create', {
|
||||
name: 'test',
|
||||
}, user);
|
||||
return res.body;
|
||||
};
|
||||
|
||||
export const page = async (user: any, page: any = {}): Promise<any> => {
|
||||
const res = await api('pages/create', {
|
||||
alignCenter: false,
|
||||
@@ -380,6 +387,96 @@ export const simpleGet = async (path: string, accept = '*/*', cookie: any = unde
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* あるAPIエンドポイントのPaginationが複数の条件で一貫した挙動であることをテストします。
|
||||
* (sinceId, untilId, sinceDate, untilDate, offset, limit)
|
||||
* @param expected 期待値となるEntityの並び(例:Note[])昇順降順が一致している必要がある
|
||||
* @param fetchEntities Entity[]を返却するテスト対象のAPIを呼び出す関数
|
||||
* @param offsetBy 何をキーとしてPaginationするか。
|
||||
* @param ordering 昇順・降順
|
||||
*/
|
||||
export async function testPaginationConsistency<Entity extends { id: string, createdAt?: string }>(
|
||||
expected: Entity[],
|
||||
fetchEntities: (paginationParam: {
|
||||
limit?: number,
|
||||
offset?: number,
|
||||
sinceId?: string,
|
||||
untilId?: string,
|
||||
sinceDate?: number,
|
||||
untilDate?: number,
|
||||
}) => Promise<Entity[]>,
|
||||
offsetBy: 'offset' | 'id' | 'createdAt' = 'id',
|
||||
ordering: 'desc' | 'asc' = 'desc'): Promise<void> {
|
||||
const rangeToParam = (p: { limit?: number, until?: Entity, since?: Entity }): object => {
|
||||
if (offsetBy === 'id') {
|
||||
return { limit: p.limit, sinceId: p.since?.id, untilId: p.until?.id };
|
||||
} else {
|
||||
const sinceDate = p.since?.createdAt !== undefined ? new Date(p.since.createdAt).getTime() : undefined;
|
||||
const untilDate = p.until?.createdAt !== undefined ? new Date(p.until.createdAt).getTime() : undefined;
|
||||
return { limit: p.limit, sinceDate, untilDate };
|
||||
}
|
||||
};
|
||||
|
||||
for (const limit of [1, 5, 10, 100, undefined]) {
|
||||
// 1. sinceId/DateとuntilId/Dateで両端を指定して取得した結果が期待通りになっていること
|
||||
if (ordering === 'desc') {
|
||||
const end = expected[expected.length - 1];
|
||||
let last = await fetchEntities(rangeToParam({ limit, since: end }));
|
||||
const actual: Entity[] = [];
|
||||
while (last.length !== 0) {
|
||||
actual.push(...last);
|
||||
last = await fetchEntities(rangeToParam({ limit, until: last[last.length - 1], since: end }));
|
||||
}
|
||||
actual.push(end);
|
||||
assert.deepStrictEqual(
|
||||
actual.map(({ id, createdAt }) => id + ':' + createdAt),
|
||||
expected.map(({ id, createdAt }) => id + ':' + createdAt));
|
||||
}
|
||||
|
||||
// 2. sinceId/Date指定+limitで取得してつなぎ合わせた結果が期待通りになっていること
|
||||
if (ordering === 'asc') {
|
||||
// 昇順にしたときの先頭(一番古いもの)をもってくる(expected[1]を基準に降順にして0番目)
|
||||
let last = await fetchEntities({ limit: 1, untilId: expected[1].id });
|
||||
const actual: Entity[] = [];
|
||||
while (last.length !== 0) {
|
||||
actual.push(...last);
|
||||
last = await fetchEntities(rangeToParam({ limit, since: last[last.length - 1] }));
|
||||
}
|
||||
assert.deepStrictEqual(
|
||||
actual.map(({ id, createdAt }) => id + ':' + createdAt),
|
||||
expected.map(({ id, createdAt }) => id + ':' + createdAt));
|
||||
}
|
||||
|
||||
// 3. untilId指定+limitで取得してつなぎ合わせた結果が期待通りになっていること
|
||||
if (ordering === 'desc') {
|
||||
let last = await fetchEntities({ limit });
|
||||
const actual: Entity[] = [];
|
||||
while (last.length !== 0) {
|
||||
actual.push(...last);
|
||||
last = await fetchEntities(rangeToParam({ limit, until: last[last.length - 1] }));
|
||||
}
|
||||
assert.deepStrictEqual(
|
||||
actual.map(({ id, createdAt }) => id + ':' + createdAt),
|
||||
expected.map(({ id, createdAt }) => id + ':' + createdAt));
|
||||
}
|
||||
|
||||
// 4. offset指定+limitで取得してつなぎ合わせた結果が期待通りになっていること
|
||||
if (offsetBy === 'offset') {
|
||||
let last = await fetchEntities({ limit, offset: 0 });
|
||||
let offset = limit ?? 10;
|
||||
const actual: Entity[] = [];
|
||||
while (last.length !== 0) {
|
||||
actual.push(...last);
|
||||
last = await fetchEntities({ limit, offset });
|
||||
offset += limit ?? 10;
|
||||
}
|
||||
assert.deepStrictEqual(
|
||||
actual.map(({ id, createdAt }) => id + ':' + createdAt),
|
||||
expected.map(({ id, createdAt }) => id + ':' + createdAt));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function initTestDb(justBorrow = false, initEntities?: any[]) {
|
||||
if (process.env.NODE_ENV !== 'test') throw 'NODE_ENV is not a test';
|
||||
|
||||
|
@@ -62,9 +62,8 @@ module.exports = {
|
||||
'vue/max-attributes-per-line': 'off',
|
||||
'vue/html-self-closing': 'off',
|
||||
'vue/singleline-html-element-content-newline': 'off',
|
||||
// (vue/vue3-recommended disabled the autofix for Vue 2 compatibility)
|
||||
'vue/v-on-event-hyphenation': ['warn', 'always', { autofix: true }],
|
||||
'vue/attribute-hyphenation': ['warn', 'never'],
|
||||
'vue/v-on-event-hyphenation': ['error', 'never', { autofix: true }],
|
||||
'vue/attribute-hyphenation': ['error', 'never'],
|
||||
},
|
||||
globals: {
|
||||
// Node.js
|
||||
|
@@ -397,6 +397,7 @@ function toStories(component: string): string {
|
||||
Promise.all([
|
||||
glob('src/components/global/*.vue'),
|
||||
glob('src/components/Mk{A,B}*.vue'),
|
||||
glob('src/components/MkDigitalClock.vue'),
|
||||
glob('src/components/MkGalleryPostPreview.vue'),
|
||||
glob('src/components/MkSignupServerRules.vue'),
|
||||
glob('src/components/MkUserSetupDialog.vue'),
|
||||
|
@@ -23,7 +23,7 @@
|
||||
"@tabler/icons-webfont": "2.17.0",
|
||||
"@vitejs/plugin-vue": "4.2.3",
|
||||
"@vue-macros/reactivity-transform": "0.3.7",
|
||||
"@vue/compiler-sfc": "3.3.2",
|
||||
"@vue/compiler-sfc": "3.3.4",
|
||||
"autosize": "6.0.1",
|
||||
"broadcast-channel": "4.20.2",
|
||||
"browser-image-resizer": "github:misskey-dev/browser-image-resizer#v2.2.1-misskey.3",
|
||||
@@ -70,8 +70,8 @@
|
||||
"typescript": "5.0.4",
|
||||
"uuid": "9.0.0",
|
||||
"vanilla-tilt": "1.8.0",
|
||||
"vite": "4.3.7",
|
||||
"vue": "3.3.2",
|
||||
"vite": "4.3.8",
|
||||
"vue": "3.3.4",
|
||||
"vue-plyr": "7.0.0",
|
||||
"vue-prism-editor": "2.0.0-alpha.2",
|
||||
"vuedraggable": "next"
|
||||
@@ -103,7 +103,7 @@
|
||||
"@types/gulp-rename": "2.0.2",
|
||||
"@types/matter-js": "0.18.3",
|
||||
"@types/micromatch": "4.0.2",
|
||||
"@types/node": "20.1.7",
|
||||
"@types/node": "20.2.1",
|
||||
"@types/punycode": "2.1.0",
|
||||
"@types/sanitize-html": "2.9.0",
|
||||
"@types/seedrandom": "3.0.5",
|
||||
@@ -115,8 +115,8 @@
|
||||
"@types/ws": "8.5.4",
|
||||
"@typescript-eslint/eslint-plugin": "5.59.5",
|
||||
"@typescript-eslint/parser": "5.59.5",
|
||||
"@vitest/coverage-c8": "0.31.0",
|
||||
"@vue/runtime-core": "3.3.2",
|
||||
"@vitest/coverage-c8": "0.31.1",
|
||||
"@vue/runtime-core": "3.3.4",
|
||||
"astring": "1.8.4",
|
||||
"chokidar-cli": "3.0.0",
|
||||
"cross-env": "7.0.3",
|
||||
@@ -125,7 +125,7 @@
|
||||
"eslint-plugin-import": "2.27.5",
|
||||
"eslint-plugin-vue": "9.13.0",
|
||||
"fast-glob": "3.2.12",
|
||||
"happy-dom": "9.18.3",
|
||||
"happy-dom": "9.19.2",
|
||||
"micromatch": "3.1.10",
|
||||
"msw": "1.2.1",
|
||||
"msw-storybook-addon": "1.8.0",
|
||||
@@ -137,7 +137,7 @@
|
||||
"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
|
||||
"summaly": "github:misskey-dev/summaly",
|
||||
"vite-plugin-turbosnap": "1.0.2",
|
||||
"vitest": "0.31.0",
|
||||
"vitest": "0.31.1",
|
||||
"vitest-fetch-mock": "0.2.2",
|
||||
"vue-eslint-parser": "9.3.0",
|
||||
"vue-tsc": "1.6.5"
|
||||
|
@@ -16,7 +16,7 @@ import { initializeSw } from '@/scripts/initialize-sw';
|
||||
|
||||
export async function mainBoot() {
|
||||
const { isClientUpdated } = await common(() => createApp(
|
||||
new URLSearchParams(window.location.search).has('zen') ? defineAsyncComponent(() => import('@/ui/zen.vue')) :
|
||||
new URLSearchParams(window.location.search).has('zen') || (ui === 'deck' && location.pathname !== '/') ? defineAsyncComponent(() => import('@/ui/zen.vue')) :
|
||||
!$i ? defineAsyncComponent(() => import('@/ui/visitor.vue')) :
|
||||
ui === 'deck' ? defineAsyncComponent(() => import('@/ui/deck.vue')) :
|
||||
ui === 'classic' ? defineAsyncComponent(() => import('@/ui/classic.vue')) :
|
||||
|
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<MkWindow ref="uiWindow" :initial-width="400" :initial-height="500" :can-resize="true" @closed="emit('closed')">
|
||||
<MkWindow ref="uiWindow" :initialWidth="400" :initialHeight="500" :canResize="true" @closed="emit('closed')">
|
||||
<template #header>
|
||||
<i class="ti ti-exclamation-circle" style="margin-right: 0.5em;"></i>
|
||||
<I18n :src="i18n.ts.reportAbuseOf" tag="span">
|
||||
@@ -8,7 +8,7 @@
|
||||
</template>
|
||||
</I18n>
|
||||
</template>
|
||||
<MkSpacer :margin-min="20" :margin-max="28">
|
||||
<MkSpacer :marginMin="20" :marginMax="28">
|
||||
<div class="_gaps_m" :class="$style.root">
|
||||
<div class="">
|
||||
<MkTextarea v-model="comment">
|
||||
|
@@ -7,11 +7,11 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { UserLite } from 'misskey-js/built/entities';
|
||||
import MkMention from './MkMention.vue';
|
||||
import { i18n } from '@/i18n';
|
||||
import { host as localHost } from '@/config';
|
||||
import { ref } from 'vue';
|
||||
import { UserLite } from 'misskey-js/built/entities';
|
||||
import { api } from '@/os';
|
||||
|
||||
const user = ref<UserLite>();
|
||||
|
@@ -1,7 +1,7 @@
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
import { StoryObj } from '@storybook/vue3';
|
||||
import isChromatic from 'chromatic/isChromatic';
|
||||
import MkAnalogClock from './MkAnalogClock.vue';
|
||||
import isChromatic from 'chromatic';
|
||||
export const Default = {
|
||||
render(args) {
|
||||
return {
|
||||
|
@@ -39,6 +39,7 @@
|
||||
-->
|
||||
|
||||
<line
|
||||
ref="sLine"
|
||||
:class="[$style.s, { [$style.animate]: !disableSAnimate && sAnimation !== 'none', [$style.elastic]: sAnimation === 'elastic', [$style.easeOut]: sAnimation === 'easeOut' }]"
|
||||
:x1="5 - (0 * (sHandLengthRatio * handsTailLength))"
|
||||
:y1="5 + (1 * (sHandLengthRatio * handsTailLength))"
|
||||
@@ -73,9 +74,10 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, onBeforeUnmount } from 'vue';
|
||||
import { computed, onMounted, onBeforeUnmount, ref } from 'vue';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import { globalEvents } from '@/events.js';
|
||||
import { defaultIdlingRenderScheduler } from '@/scripts/idle-render.js';
|
||||
|
||||
// https://stackoverflow.com/questions/1878907/how-can-i-find-the-difference-between-two-angles
|
||||
const angleDiff = (a: number, b: number) => {
|
||||
@@ -145,6 +147,7 @@ let mAngle = $ref<number>(0);
|
||||
let sAngle = $ref<number>(0);
|
||||
let disableSAnimate = $ref(false);
|
||||
let sOneRound = false;
|
||||
const sLine = ref<SVGPathElement>();
|
||||
|
||||
function tick() {
|
||||
const now = props.now();
|
||||
@@ -160,17 +163,21 @@ function tick() {
|
||||
}
|
||||
hAngle = Math.PI * (h % (props.twentyfour ? 24 : 12) + (m + s / 60) / 60) / (props.twentyfour ? 12 : 6);
|
||||
mAngle = Math.PI * (m + s / 60) / 30;
|
||||
if (sOneRound) { // 秒針が一周した際のアニメーションをよしなに処理する(これが無いと秒が59->0になったときに期待したアニメーションにならない)
|
||||
if (sOneRound && sLine.value) { // 秒針が一周した際のアニメーションをよしなに処理する(これが無いと秒が59->0になったときに期待したアニメーションにならない)
|
||||
sAngle = Math.PI * 60 / 30;
|
||||
window.setTimeout(() => {
|
||||
defaultIdlingRenderScheduler.delete(tick);
|
||||
sLine.value.addEventListener('transitionend', () => {
|
||||
disableSAnimate = true;
|
||||
window.setTimeout(() => {
|
||||
requestAnimationFrame(() => {
|
||||
sAngle = 0;
|
||||
window.setTimeout(() => {
|
||||
requestAnimationFrame(() => {
|
||||
disableSAnimate = false;
|
||||
}, 100);
|
||||
}, 100);
|
||||
}, 700);
|
||||
if (enabled) {
|
||||
defaultIdlingRenderScheduler.add(tick);
|
||||
}
|
||||
});
|
||||
});
|
||||
}, { once: true });
|
||||
} else {
|
||||
sAngle = Math.PI * s / 30;
|
||||
}
|
||||
@@ -194,20 +201,13 @@ function calcColors() {
|
||||
calcColors();
|
||||
|
||||
onMounted(() => {
|
||||
const update = () => {
|
||||
if (enabled) {
|
||||
tick();
|
||||
window.setTimeout(update, 1000);
|
||||
}
|
||||
};
|
||||
update();
|
||||
|
||||
defaultIdlingRenderScheduler.add(tick);
|
||||
globalEvents.on('themeChanged', calcColors);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
enabled = false;
|
||||
|
||||
defaultIdlingRenderScheduler.delete(tick);
|
||||
globalEvents.off('themeChanged', calcColors);
|
||||
});
|
||||
</script>
|
||||
|
@@ -11,29 +11,29 @@
|
||||
<div v-else-if="c.type === 'buttons'" class="_buttons" :style="{ justifyContent: align }">
|
||||
<MkButton v-for="button in c.buttons" :primary="button.primary" :rounded="button.rounded" :disabled="button.disabled" inline :small="size === 'small'" @click="button.onClick">{{ button.text }}</MkButton>
|
||||
</div>
|
||||
<MkSwitch v-else-if="c.type === 'switch'" :model-value="valueForSwitch" @update:model-value="onSwitchUpdate">
|
||||
<MkSwitch v-else-if="c.type === 'switch'" :modelValue="valueForSwitch" @update:modelValue="onSwitchUpdate">
|
||||
<template v-if="c.label" #label>{{ c.label }}</template>
|
||||
<template v-if="c.caption" #caption>{{ c.caption }}</template>
|
||||
</MkSwitch>
|
||||
<MkTextarea v-else-if="c.type === 'textarea'" :model-value="c.default" @update:model-value="c.onInput">
|
||||
<MkTextarea v-else-if="c.type === 'textarea'" :modelValue="c.default" @update:modelValue="c.onInput">
|
||||
<template v-if="c.label" #label>{{ c.label }}</template>
|
||||
<template v-if="c.caption" #caption>{{ c.caption }}</template>
|
||||
</MkTextarea>
|
||||
<MkInput v-else-if="c.type === 'textInput'" :small="size === 'small'" :model-value="c.default" @update:model-value="c.onInput">
|
||||
<MkInput v-else-if="c.type === 'textInput'" :small="size === 'small'" :modelValue="c.default" @update:modelValue="c.onInput">
|
||||
<template v-if="c.label" #label>{{ c.label }}</template>
|
||||
<template v-if="c.caption" #caption>{{ c.caption }}</template>
|
||||
</MkInput>
|
||||
<MkInput v-else-if="c.type === 'numberInput'" :small="size === 'small'" :model-value="c.default" type="number" @update:model-value="c.onInput">
|
||||
<MkInput v-else-if="c.type === 'numberInput'" :small="size === 'small'" :modelValue="c.default" type="number" @update:modelValue="c.onInput">
|
||||
<template v-if="c.label" #label>{{ c.label }}</template>
|
||||
<template v-if="c.caption" #caption>{{ c.caption }}</template>
|
||||
</MkInput>
|
||||
<MkSelect v-else-if="c.type === 'select'" :small="size === 'small'" :model-value="c.default" @update:model-value="c.onChange">
|
||||
<MkSelect v-else-if="c.type === 'select'" :small="size === 'small'" :modelValue="c.default" @update:modelValue="c.onChange">
|
||||
<template v-if="c.label" #label>{{ c.label }}</template>
|
||||
<template v-if="c.caption" #caption>{{ c.caption }}</template>
|
||||
<option v-for="item in c.items" :key="item.value" :value="item.value">{{ item.text }}</option>
|
||||
</MkSelect>
|
||||
<MkButton v-else-if="c.type === 'postFormButton'" :primary="c.primary" :rounded="c.rounded" :small="size === 'small'" inline @click="openPostForm">{{ c.text }}</MkButton>
|
||||
<MkFolder v-else-if="c.type === 'folder'" :default-open="c.opened">
|
||||
<MkFolder v-else-if="c.type === 'folder'" :defaultOpen="c.opened">
|
||||
<template #label>{{ c.title }}</template>
|
||||
<template v-for="child in c.children" :key="child">
|
||||
<MkAsUi v-if="!g(child).hidden" :component="g(child)" :components="props.components" :size="size"/>
|
||||
|
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-for="user in users" :key="user.id" style="display:inline-block;width:32px;height:32px;margin-right:8px;">
|
||||
<MkAvatar :user="user" style="width:32px;height:32px;" indicator link preview/>
|
||||
<MkAvatar :user="user" style="width:32px; height:32px;" indicator link preview/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<div class="cbbedffa">
|
||||
<div :class="$style.root">
|
||||
<canvas ref="chartEl"></canvas>
|
||||
<MkChartLegend ref="legendEl" style="margin-top: 8px;"/>
|
||||
<div v-if="fetching" class="fetching">
|
||||
<div v-if="fetching" :class="$style.fetching">
|
||||
<MkLoading/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -817,22 +817,22 @@ onMounted(() => {
|
||||
/* eslint-enable id-denylist */
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.cbbedffa {
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
> .fetching {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
-webkit-backdrop-filter: var(--blur, blur(12px));
|
||||
backdrop-filter: var(--blur, blur(12px));
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: wait;
|
||||
}
|
||||
.fetching {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
-webkit-backdrop-filter: var(--blur, blur(12px));
|
||||
backdrop-filter: var(--blur, blur(12px));
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: wait;
|
||||
}
|
||||
</style>
|
||||
|
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<MkTooltip ref="tooltip" :showing="showing" :x="x" :y="y" :max-width="340" :direction="'top'" :inner-margin="16" @closed="emit('closed')">
|
||||
<MkTooltip ref="tooltip" :showing="showing" :x="x" :y="y" :maxWidth="340" :direction="'top'" :innerMargin="16" @closed="emit('closed')">
|
||||
<div v-if="title || series">
|
||||
<div v-if="title" :class="$style.title">{{ title }}</div>
|
||||
<template v-if="series">
|
||||
|
@@ -6,7 +6,7 @@
|
||||
<slot name="header"></slot>
|
||||
</div>
|
||||
<div :class="$style.headerSub">
|
||||
<slot name="func" :button-style-class="$style.headerButton"></slot>
|
||||
<slot name="func" :buttonStyleClass="$style.headerButton"></slot>
|
||||
<button v-if="foldable" :class="$style.headerButton" class="_button" @click="() => showBody = !showBody">
|
||||
<template v-if="showBody"><i class="ti ti-chevron-up"></i></template>
|
||||
<template v-else><i class="ti ti-chevron-down"></i></template>
|
||||
@@ -14,14 +14,14 @@
|
||||
</div>
|
||||
</header>
|
||||
<Transition
|
||||
:enter-active-class="defaultStore.state.animation ? $style.transition_toggle_enterActive : ''"
|
||||
:leave-active-class="defaultStore.state.animation ? $style.transition_toggle_leaveActive : ''"
|
||||
:enter-from-class="defaultStore.state.animation ? $style.transition_toggle_enterFrom : ''"
|
||||
:leave-to-class="defaultStore.state.animation ? $style.transition_toggle_leaveTo : ''"
|
||||
:enterActiveClass="defaultStore.state.animation ? $style.transition_toggle_enterActive : ''"
|
||||
:leaveActiveClass="defaultStore.state.animation ? $style.transition_toggle_leaveActive : ''"
|
||||
:enterFromClass="defaultStore.state.animation ? $style.transition_toggle_enterFrom : ''"
|
||||
:leaveToClass="defaultStore.state.animation ? $style.transition_toggle_leaveTo : ''"
|
||||
@enter="enter"
|
||||
@after-enter="afterEnter"
|
||||
@afterEnter="afterEnter"
|
||||
@leave="leave"
|
||||
@after-leave="afterLeave"
|
||||
@afterLeave="afterLeave"
|
||||
>
|
||||
<div v-show="showBody" ref="contentEl" :class="[$style.content, { [$style.omitted]: omitted }]">
|
||||
<slot></slot>
|
||||
|
@@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<Transition
|
||||
appear
|
||||
:enter-active-class="defaultStore.state.animation ? $style.transition_fade_enterActive : ''"
|
||||
:leave-active-class="defaultStore.state.animation ? $style.transition_fade_leaveActive : ''"
|
||||
:enter-from-class="defaultStore.state.animation ? $style.transition_fade_enterFrom : ''"
|
||||
:leave-to-class="defaultStore.state.animation ? $style.transition_fade_leaveTo : ''"
|
||||
:enterActiveClass="defaultStore.state.animation ? $style.transition_fade_enterActive : ''"
|
||||
:leaveActiveClass="defaultStore.state.animation ? $style.transition_fade_leaveActive : ''"
|
||||
:enterFromClass="defaultStore.state.animation ? $style.transition_fade_enterFrom : ''"
|
||||
:leaveToClass="defaultStore.state.animation ? $style.transition_fade_leaveTo : ''"
|
||||
>
|
||||
<div ref="rootEl" :class="$style.root" :style="{ zIndex }" @contextmenu.prevent.stop="() => {}">
|
||||
<MkMenu :items="items" :align="'left'" @close="$emit('closed')"/>
|
||||
|
@@ -4,7 +4,7 @@
|
||||
:width="800"
|
||||
:height="500"
|
||||
:scroll="false"
|
||||
:with-ok-button="true"
|
||||
:withOkButton="true"
|
||||
@close="cancel()"
|
||||
@ok="ok()"
|
||||
@closed="$emit('closed')"
|
||||
|
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<MkModal ref="modal" :prefer-type="'dialog'" :z-priority="'high'" @click="done(true)" @closed="emit('closed')">
|
||||
<MkModal ref="modal" :preferType="'dialog'" :zPriority="'high'" @click="done(true)" @closed="emit('closed')">
|
||||
<div :class="$style.root">
|
||||
<div v-if="icon" :class="$style.icon">
|
||||
<i :class="icon"></i>
|
||||
|
@@ -0,0 +1,32 @@
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
import { StoryObj } from '@storybook/vue3';
|
||||
import isChromatic from 'chromatic/isChromatic';
|
||||
import MkDigitalClock from './MkDigitalClock.vue';
|
||||
export const Default = {
|
||||
render(args) {
|
||||
return {
|
||||
components: {
|
||||
MkDigitalClock,
|
||||
},
|
||||
setup() {
|
||||
return {
|
||||
args,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
props() {
|
||||
return {
|
||||
...this.args,
|
||||
};
|
||||
},
|
||||
},
|
||||
template: '<MkDigitalClock v-bind="props" />',
|
||||
};
|
||||
},
|
||||
args: {
|
||||
now: isChromatic() ? () => new Date('2023-01-01T10:10:30') : undefined,
|
||||
},
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
},
|
||||
} satisfies StoryObj<typeof MkDigitalClock>;
|
@@ -11,19 +11,21 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onUnmounted, ref, watch } from 'vue';
|
||||
import { onMounted, onUnmounted, ref, watch } from 'vue';
|
||||
import { defaultIdlingRenderScheduler } from '@/scripts/idle-render.js';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
showS?: boolean;
|
||||
showMs?: boolean;
|
||||
offset?: number;
|
||||
now?: () => Date;
|
||||
}>(), {
|
||||
showS: true,
|
||||
showMs: false,
|
||||
offset: 0 - new Date().getTimezoneOffset(),
|
||||
now: () => new Date(),
|
||||
});
|
||||
|
||||
let intervalId;
|
||||
const hh = ref('');
|
||||
const mm = ref('');
|
||||
const ss = ref('');
|
||||
@@ -39,9 +41,9 @@ watch(showColon, (v) => {
|
||||
}
|
||||
});
|
||||
|
||||
const tick = () => {
|
||||
const now = new Date();
|
||||
now.setMinutes(now.getMinutes() + (new Date().getTimezoneOffset() + props.offset));
|
||||
const tick = (): void => {
|
||||
const now = props.now();
|
||||
now.setMinutes(now.getMinutes() + now.getTimezoneOffset() + props.offset);
|
||||
hh.value = now.getHours().toString().padStart(2, '0');
|
||||
mm.value = now.getMinutes().toString().padStart(2, '0');
|
||||
ss.value = now.getSeconds().toString().padStart(2, '0');
|
||||
@@ -52,13 +54,12 @@ const tick = () => {
|
||||
|
||||
tick();
|
||||
|
||||
watch(() => props.showMs, () => {
|
||||
if (intervalId) window.clearInterval(intervalId);
|
||||
intervalId = window.setInterval(tick, props.showMs ? 10 : 1000);
|
||||
}, { immediate: true });
|
||||
onMounted(() => {
|
||||
defaultIdlingRenderScheduler.add(tick);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.clearInterval(intervalId);
|
||||
defaultIdlingRenderScheduler.delete(tick);
|
||||
});
|
||||
</script>
|
||||
|
||||
|
@@ -1,13 +1,13 @@
|
||||
<template>
|
||||
<div class="drylbebk"
|
||||
:class="{ draghover }"
|
||||
<div
|
||||
:class="[$style.root, { [$style.draghover]: draghover }]"
|
||||
@click="onClick"
|
||||
@dragover.prevent.stop="onDragover"
|
||||
@dragenter="onDragenter"
|
||||
@dragleave="onDragleave"
|
||||
@drop.stop="onDrop"
|
||||
>
|
||||
<i v-if="folder == null" class="ti ti-cloud"></i>
|
||||
<i v-if="folder == null" class="ti ti-cloud" style="margin-right: 4px;"></i>
|
||||
<span>{{ folder == null ? i18n.ts.drive : folder.name }}</span>
|
||||
</div>
|
||||
</template>
|
||||
@@ -130,18 +130,10 @@ function onDrop(ev: DragEvent) {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.drylbebk {
|
||||
> * {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
&.draghover {
|
||||
background: #eee;
|
||||
}
|
||||
|
||||
> i {
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@@ -4,21 +4,21 @@
|
||||
<div class="path" @contextmenu.prevent.stop="() => {}">
|
||||
<XNavFolder
|
||||
:class="{ current: folder == null }"
|
||||
:parent-folder="folder"
|
||||
:parentFolder="folder"
|
||||
@move="move"
|
||||
@upload="upload"
|
||||
@remove-file="removeFile"
|
||||
@remove-folder="removeFolder"
|
||||
@removeFile="removeFile"
|
||||
@removeFolder="removeFolder"
|
||||
/>
|
||||
<template v-for="f in hierarchyFolders">
|
||||
<span class="separator"><i class="ti ti-chevron-right"></i></span>
|
||||
<XNavFolder
|
||||
:folder="f"
|
||||
:parent-folder="folder"
|
||||
:parentFolder="folder"
|
||||
@move="move"
|
||||
@upload="upload"
|
||||
@remove-file="removeFile"
|
||||
@remove-folder="removeFolder"
|
||||
@removeFile="removeFile"
|
||||
@removeFolder="removeFolder"
|
||||
/>
|
||||
</template>
|
||||
<span v-if="folder != null" class="separator"><i class="ti ti-chevron-right"></i></span>
|
||||
@@ -43,13 +43,13 @@
|
||||
v-anim="i"
|
||||
class="folder"
|
||||
:folder="f"
|
||||
:select-mode="select === 'folder'"
|
||||
:is-selected="selectedFolders.some(x => x.id === f.id)"
|
||||
:selectMode="select === 'folder'"
|
||||
:isSelected="selectedFolders.some(x => x.id === f.id)"
|
||||
@chosen="chooseFolder"
|
||||
@move="move"
|
||||
@upload="upload"
|
||||
@remove-file="removeFile"
|
||||
@remove-folder="removeFolder"
|
||||
@removeFile="removeFile"
|
||||
@removeFolder="removeFolder"
|
||||
@dragstart="isDragSource = true"
|
||||
@dragend="isDragSource = false"
|
||||
/>
|
||||
@@ -64,8 +64,8 @@
|
||||
v-anim="i"
|
||||
class="file"
|
||||
:file="file"
|
||||
:select-mode="select === 'file'"
|
||||
:is-selected="selectedFiles.some(x => x.id === file.id)"
|
||||
:selectMode="select === 'file'"
|
||||
:isSelected="selectedFiles.some(x => x.id === file.id)"
|
||||
@chosen="chooseFile"
|
||||
@dragstart="isDragSource = true"
|
||||
@dragend="isDragSource = false"
|
||||
|
@@ -1,16 +1,16 @@
|
||||
<template>
|
||||
<div ref="thumbnail" class="zdjebgpv">
|
||||
<div ref="thumbnail" :class="$style.root">
|
||||
<ImgWithBlurhash v-if="isThumbnailAvailable" :hash="file.blurhash" :src="file.thumbnailUrl" :alt="file.name" :title="file.name" :cover="fit !== 'contain'"/>
|
||||
<i v-else-if="is === 'image'" class="ti ti-photo icon"></i>
|
||||
<i v-else-if="is === 'video'" class="ti ti-video icon"></i>
|
||||
<i v-else-if="is === 'audio' || is === 'midi'" class="ti ti-file-music icon"></i>
|
||||
<i v-else-if="is === 'csv'" class="ti ti-file-text icon"></i>
|
||||
<i v-else-if="is === 'pdf'" class="ti ti-file-text icon"></i>
|
||||
<i v-else-if="is === 'textfile'" class="ti ti-file-text icon"></i>
|
||||
<i v-else-if="is === 'archive'" class="ti ti-file-zip icon"></i>
|
||||
<i v-else class="ti ti-file icon"></i>
|
||||
<i v-else-if="is === 'image'" class="ti ti-photo" :class="$style.icon"></i>
|
||||
<i v-else-if="is === 'video'" class="ti ti-video" :class="$style.icon"></i>
|
||||
<i v-else-if="is === 'audio' || is === 'midi'" class="ti ti-file-music" :class="$style.icon"></i>
|
||||
<i v-else-if="is === 'csv'" class="ti ti-file-text" :class="$style.icon"></i>
|
||||
<i v-else-if="is === 'pdf'" class="ti ti-file-text" :class="$style.icon"></i>
|
||||
<i v-else-if="is === 'textfile'" class="ti ti-file-text" :class="$style.icon"></i>
|
||||
<i v-else-if="is === 'archive'" class="ti ti-file-zip" :class="$style.icon"></i>
|
||||
<i v-else class="ti ti-file" :class="$style.icon"></i>
|
||||
|
||||
<i v-if="isThumbnailAvailable && is === 'video'" class="ti ti-video icon-sub"></i>
|
||||
<i v-if="isThumbnailAvailable && is === 'video'" class="ti ti-video" :class="$style.iconSub"></i>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -53,28 +53,28 @@ const isThumbnailAvailable = computed(() => {
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.zdjebgpv {
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
position: relative;
|
||||
display: flex;
|
||||
background: var(--panel);
|
||||
border-radius: 8px;
|
||||
overflow: clip;
|
||||
}
|
||||
|
||||
> .icon-sub {
|
||||
position: absolute;
|
||||
width: 30%;
|
||||
height: auto;
|
||||
margin: 0;
|
||||
right: 4%;
|
||||
bottom: 4%;
|
||||
}
|
||||
.iconSub {
|
||||
position: absolute;
|
||||
width: 30%;
|
||||
height: auto;
|
||||
margin: 0;
|
||||
right: 4%;
|
||||
bottom: 4%;
|
||||
}
|
||||
|
||||
> .icon {
|
||||
pointer-events: none;
|
||||
margin: auto;
|
||||
font-size: 32px;
|
||||
color: #777;
|
||||
}
|
||||
.icon {
|
||||
pointer-events: none;
|
||||
margin: auto;
|
||||
font-size: 32px;
|
||||
color: #777;
|
||||
}
|
||||
</style>
|
||||
|
@@ -3,8 +3,8 @@
|
||||
ref="dialog"
|
||||
:width="800"
|
||||
:height="500"
|
||||
:with-ok-button="true"
|
||||
:ok-button-disabled="(type === 'file') && (selected.length === 0)"
|
||||
:withOkButton="true"
|
||||
:okButtonDisabled="(type === 'file') && (selected.length === 0)"
|
||||
@click="cancel()"
|
||||
@close="cancel()"
|
||||
@ok="ok()"
|
||||
@@ -14,7 +14,7 @@
|
||||
{{ multiple ? ((type === 'file') ? i18n.ts.selectFiles : i18n.ts.selectFolders) : ((type === 'file') ? i18n.ts.selectFile : i18n.ts.selectFolder) }}
|
||||
<span v-if="selected.length > 0" style="margin-left: 8px; opacity: 0.5;">({{ number(selected.length) }})</span>
|
||||
</template>
|
||||
<XDrive :multiple="multiple" :select="type" @change-selection="onChangeSelection" @selected="ok()"/>
|
||||
<XDrive :multiple="multiple" :select="type" @changeSelection="onChangeSelection" @selected="ok()"/>
|
||||
</MkModalWindow>
|
||||
</template>
|
||||
|
||||
|
@@ -1,15 +1,15 @@
|
||||
<template>
|
||||
<MkWindow
|
||||
ref="window"
|
||||
:initial-width="800"
|
||||
:initial-height="500"
|
||||
:can-resize="true"
|
||||
:initialWidth="800"
|
||||
:initialHeight="500"
|
||||
:canResize="true"
|
||||
@closed="emit('closed')"
|
||||
>
|
||||
<template #header>
|
||||
{{ i18n.ts.drive }}
|
||||
</template>
|
||||
<XDrive :initial-folder="initialFolder"/>
|
||||
<XDrive :initialFolder="initialFolder"/>
|
||||
</MkWindow>
|
||||
</template>
|
||||
|
||||
|
@@ -2,10 +2,10 @@
|
||||
<MkModal
|
||||
ref="modal"
|
||||
v-slot="{ type, maxHeight }"
|
||||
:z-priority="'middle'"
|
||||
:prefer-type="asReactionPicker && defaultStore.state.reactionPickerUseDrawerForMobile === false ? 'popup' : 'auto'"
|
||||
:transparent-bg="true"
|
||||
:manual-showing="manualShowing"
|
||||
:zPriority="'middle'"
|
||||
:preferType="asReactionPicker && defaultStore.state.reactionPickerUseDrawerForMobile === false ? 'popup' : 'auto'"
|
||||
:transparentBg="true"
|
||||
:manualShowing="manualShowing"
|
||||
:src="src"
|
||||
@click="modal?.close()"
|
||||
@opening="opening"
|
||||
@@ -16,9 +16,9 @@
|
||||
ref="picker"
|
||||
class="ryghynhb _popup _shadow"
|
||||
:class="{ drawer: type === 'drawer' }"
|
||||
:show-pinned="showPinned"
|
||||
:as-reaction-picker="asReactionPicker"
|
||||
:as-drawer="type === 'drawer'"
|
||||
:showPinned="showPinned"
|
||||
:asReactionPicker="asReactionPicker"
|
||||
:asDrawer="type === 'drawer'"
|
||||
:max-height="maxHeight"
|
||||
@chosen="chosen"
|
||||
/>
|
||||
|
@@ -1,13 +1,14 @@
|
||||
<template>
|
||||
<MkWindow ref="window"
|
||||
:initial-width="300"
|
||||
:initial-height="290"
|
||||
:can-resize="true"
|
||||
<MkWindow
|
||||
ref="window"
|
||||
:initialWidth="300"
|
||||
:initialHeight="290"
|
||||
:canResize="true"
|
||||
:mini="true"
|
||||
:front="true"
|
||||
@closed="emit('closed')"
|
||||
>
|
||||
<MkEmojiPicker :show-pinned="showPinned" :as-reaction-picker="asReactionPicker" as-window :class="$style.picker" @chosen="chosen"/>
|
||||
<MkEmojiPicker :showPinned="showPinned" :asReactionPicker="asReactionPicker" asWindow :class="$style.picker" @chosen="chosen"/>
|
||||
</MkWindow>
|
||||
</template>
|
||||
|
||||
|
@@ -3,14 +3,14 @@
|
||||
ref="dialog"
|
||||
:width="400"
|
||||
:height="450"
|
||||
:with-ok-button="true"
|
||||
:ok-button-disabled="false"
|
||||
:withOkButton="true"
|
||||
:okButtonDisabled="false"
|
||||
@ok="ok()"
|
||||
@close="dialog.close()"
|
||||
@closed="emit('closed')"
|
||||
>
|
||||
<template #header>{{ i18n.ts.describeFile }}</template>
|
||||
<MkSpacer :margin-min="20" :margin-max="28">
|
||||
<MkSpacer :marginMin="20" :marginMax="28">
|
||||
<MkDriveFileThumbnail :file="file" fit="contain" style="height: 100px; margin-bottom: 16px;"/>
|
||||
<MkTextarea v-model="caption" autofocus :placeholder="i18n.ts.inputNewDescription">
|
||||
<template #label>{{ i18n.ts.caption }}</template>
|
||||
|
@@ -1,9 +1,9 @@
|
||||
<template>
|
||||
<div ref="el" class="ssazuxis">
|
||||
<header class="_button" :style="{ background: bg }" @click="showBody = !showBody">
|
||||
<div class="title"><div><slot name="header"></slot></div></div>
|
||||
<div class="divider"></div>
|
||||
<button class="_button">
|
||||
<div ref="el" :class="$style.root">
|
||||
<header :class="$style.header" class="_button" :style="{ background: bg }" @click="showBody = !showBody">
|
||||
<div :class="$style.title"><div><slot name="header"></slot></div></div>
|
||||
<div :class="$style.divider"></div>
|
||||
<button class="_button" :class="$style.button">
|
||||
<template v-if="showBody"><i class="ti ti-chevron-up"></i></template>
|
||||
<template v-else><i class="ti ti-chevron-down"></i></template>
|
||||
</button>
|
||||
@@ -11,9 +11,9 @@
|
||||
<Transition
|
||||
:name="defaultStore.state.animation ? 'folder-toggle' : ''"
|
||||
@enter="enter"
|
||||
@after-enter="afterEnter"
|
||||
@afterEnter="afterEnter"
|
||||
@leave="leave"
|
||||
@after-leave="afterLeave"
|
||||
@afterLeave="afterLeave"
|
||||
>
|
||||
<div v-show="showBody">
|
||||
<slot></slot>
|
||||
@@ -86,7 +86,7 @@ onMounted(() => {
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
<style lang="scss" module>
|
||||
.folder-toggle-enter-active, .folder-toggle-leave-active {
|
||||
overflow-y: clip;
|
||||
transition: opacity 0.5s, height 0.5s !important;
|
||||
@@ -98,45 +98,41 @@ onMounted(() => {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.ssazuxis {
|
||||
.root {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
> header {
|
||||
display: flex;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
position: sticky;
|
||||
top: var(--stickyTop, 0px);
|
||||
-webkit-backdrop-filter: var(--blur, blur(8px));
|
||||
backdrop-filter: var(--blur, blur(20px));
|
||||
.header {
|
||||
display: flex;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
position: sticky;
|
||||
top: var(--stickyTop, 0px);
|
||||
-webkit-backdrop-filter: var(--blur, blur(8px));
|
||||
backdrop-filter: var(--blur, blur(20px));
|
||||
}
|
||||
|
||||
> .title {
|
||||
display: grid;
|
||||
place-content: center;
|
||||
margin: 0;
|
||||
padding: 12px 16px 12px 0;
|
||||
}
|
||||
.title {
|
||||
display: grid;
|
||||
place-content: center;
|
||||
margin: 0;
|
||||
padding: 12px 16px 12px 0;
|
||||
}
|
||||
|
||||
> .divider {
|
||||
flex: 1;
|
||||
margin: auto;
|
||||
height: 1px;
|
||||
background: var(--divider);
|
||||
}
|
||||
.divider {
|
||||
flex: 1;
|
||||
margin: auto;
|
||||
height: 1px;
|
||||
background: var(--divider);
|
||||
}
|
||||
|
||||
> button {
|
||||
padding: 12px 0 12px 16px;
|
||||
}
|
||||
}
|
||||
.button {
|
||||
padding: 12px 0 12px 16px;
|
||||
}
|
||||
|
||||
@container (max-width: 500px) {
|
||||
.ssazuxis {
|
||||
> header {
|
||||
> .title {
|
||||
padding: 8px 10px 8px 0;
|
||||
}
|
||||
}
|
||||
.title {
|
||||
padding: 8px 10px 8px 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@@ -6,7 +6,7 @@
|
||||
<div :class="$style.headerIcon"><slot name="icon"></slot></div>
|
||||
<div :class="$style.headerText">
|
||||
<div :class="$style.headerTextMain">
|
||||
<MkCondensedLine :min-scale="2 / 3"><slot name="label"></slot></MkCondensedLine>
|
||||
<MkCondensedLine :minScale="2 / 3"><slot name="label"></slot></MkCondensedLine>
|
||||
</div>
|
||||
<div :class="$style.headerTextSub">
|
||||
<slot name="caption"></slot>
|
||||
@@ -22,18 +22,18 @@
|
||||
|
||||
<div v-if="openedAtLeastOnce" :class="[$style.body, { [$style.bgSame]: bgSame }]" :style="{ maxHeight: maxHeight ? `${maxHeight}px` : null, overflow: maxHeight ? `auto` : null }" :aria-hidden="!opened">
|
||||
<Transition
|
||||
:enter-active-class="defaultStore.state.animation ? $style.transition_toggle_enterActive : ''"
|
||||
:leave-active-class="defaultStore.state.animation ? $style.transition_toggle_leaveActive : ''"
|
||||
:enter-from-class="defaultStore.state.animation ? $style.transition_toggle_enterFrom : ''"
|
||||
:leave-to-class="defaultStore.state.animation ? $style.transition_toggle_leaveTo : ''"
|
||||
:enterActiveClass="defaultStore.state.animation ? $style.transition_toggle_enterActive : ''"
|
||||
:leaveActiveClass="defaultStore.state.animation ? $style.transition_toggle_leaveActive : ''"
|
||||
:enterFromClass="defaultStore.state.animation ? $style.transition_toggle_enterFrom : ''"
|
||||
:leaveToClass="defaultStore.state.animation ? $style.transition_toggle_leaveTo : ''"
|
||||
@enter="enter"
|
||||
@after-enter="afterEnter"
|
||||
@afterEnter="afterEnter"
|
||||
@leave="leave"
|
||||
@after-leave="afterLeave"
|
||||
@afterLeave="afterLeave"
|
||||
>
|
||||
<KeepAlive>
|
||||
<div v-show="opened">
|
||||
<MkSpacer :margin-min="14" :margin-max="22">
|
||||
<MkSpacer :marginMin="14" :marginMax="22">
|
||||
<slot></slot>
|
||||
</MkSpacer>
|
||||
</div>
|
||||
|
@@ -2,9 +2,9 @@
|
||||
<MkModalWindow
|
||||
ref="dialog"
|
||||
:width="450"
|
||||
:can-close="false"
|
||||
:with-ok-button="true"
|
||||
:ok-button-disabled="false"
|
||||
:canClose="false"
|
||||
:withOkButton="true"
|
||||
:okButtonDisabled="false"
|
||||
@click="cancel()"
|
||||
@ok="ok()"
|
||||
@close="cancel()"
|
||||
@@ -14,7 +14,7 @@
|
||||
{{ title }}
|
||||
</template>
|
||||
|
||||
<MkSpacer :margin-min="20" :margin-max="32">
|
||||
<MkSpacer :marginMin="20" :marginMax="32">
|
||||
<div class="_gaps_m">
|
||||
<template v-for="item in Object.keys(form).filter(item => !form[item].hidden)">
|
||||
<MkInput v-if="form[item].type === 'number'" v-model="values[item]" type="number" :step="form[item].step || 1">
|
||||
@@ -41,7 +41,7 @@
|
||||
<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ i18n.ts.optional }})</span></template>
|
||||
<option v-for="item in form[item].options" :key="item.value" :value="item.value">{{ item.label }}</option>
|
||||
</MkRadios>
|
||||
<MkRange v-else-if="form[item].type === 'range'" v-model="values[item]" :min="form[item].min" :max="form[item].max" :step="form[item].step" :text-converter="form[item].textConverter">
|
||||
<MkRange v-else-if="form[item].type === 'range'" v-model="values[item]" :min="form[item].min" :max="form[item].max" :step="form[item].step" :textConverter="form[item].textConverter">
|
||||
<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ i18n.ts.optional }})</span></template>
|
||||
<template v-if="form[item].description" #caption>{{ form[item].description }}</template>
|
||||
</MkRange>
|
||||
|
@@ -11,7 +11,7 @@
|
||||
}"
|
||||
:src="post.files[0].thumbnailUrl"
|
||||
:hash="post.files[0].blurhash"
|
||||
:force-blurhash="!show"
|
||||
:forceBlurhash="!show"
|
||||
/>
|
||||
</Transition>
|
||||
</div>
|
||||
|
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<MkModal ref="modal" :z-priority="'middle'" @click="modal.close()" @closed="emit('closed')">
|
||||
<MkModal ref="modal" :zPriority="'middle'" @click="modal.close()" @closed="emit('closed')">
|
||||
<div class="xubzgfga">
|
||||
<header>{{ image.name }}</header>
|
||||
<img :src="image.url" :alt="image.comment" :title="image.comment" @click="modal.close()"/>
|
||||
|
@@ -2,12 +2,12 @@
|
||||
<div ref="root" :class="[$style.root, { [$style.cover]: cover }]" :title="title ?? ''">
|
||||
<TransitionGroup
|
||||
:duration="defaultStore.state.animation && props.transition?.duration || undefined"
|
||||
:enter-active-class="defaultStore.state.animation && props.transition?.enterActiveClass || undefined"
|
||||
:leave-active-class="defaultStore.state.animation && (props.transition?.leaveActiveClass ?? $style['transition_leaveActive']) || undefined"
|
||||
:enter-from-class="defaultStore.state.animation && props.transition?.enterFromClass || undefined"
|
||||
:leave-to-class="defaultStore.state.animation && props.transition?.leaveToClass || undefined"
|
||||
:enter-to-class="defaultStore.state.animation && props.transition?.enterToClass || undefined"
|
||||
:leave-from-class="defaultStore.state.animation && props.transition?.leaveFromClass || undefined"
|
||||
:enterActiveClass="defaultStore.state.animation && props.transition?.enterActiveClass || undefined"
|
||||
:leaveActiveClass="defaultStore.state.animation && (props.transition?.leaveActiveClass ?? $style['transition_leaveActive']) || undefined"
|
||||
:enterFromClass="defaultStore.state.animation && props.transition?.enterFromClass || undefined"
|
||||
:leaveToClass="defaultStore.state.animation && props.transition?.leaveToClass || undefined"
|
||||
:enterToClass="defaultStore.state.animation && props.transition?.enterToClass || undefined"
|
||||
:leaveFromClass="defaultStore.state.animation && props.transition?.leaveFromClass || undefined"
|
||||
>
|
||||
<canvas v-show="hide" key="canvas" ref="canvas" :class="$style.canvas" :width="canvasWidth" :height="canvasHeight" :title="title ?? undefined"/>
|
||||
<img v-show="!hide" key="img" ref="img" :height="imgHeight" :width="imgWidth" :class="$style.img" :src="src ?? undefined" :title="title ?? undefined" :alt="alt ?? undefined" loading="eager" decoding="async"/>
|
||||
@@ -16,10 +16,10 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { $ref } from 'vue/macros';
|
||||
import DrawBlurhash from '@/workers/draw-blurhash?worker';
|
||||
import TestWebGL2 from '@/workers/test-webgl2?worker';
|
||||
import { WorkerMultiDispatch } from '@/scripts/worker-multi-dispatch';
|
||||
import { $ref } from 'vue/macros';
|
||||
import { extractAvgColorFromBlurhash } from '@/scripts/extract-avg-color-from-blurhash';
|
||||
|
||||
const workerPromise = new Promise<WorkerMultiDispatch | null>(resolve => {
|
||||
|
@@ -1,9 +1,9 @@
|
||||
<template>
|
||||
<div class="alqyeyti" :class="{ oneline }">
|
||||
<div class="key">
|
||||
<div :class="[$style.root, { [$style.oneline]: oneline }]">
|
||||
<div :class="$style.key">
|
||||
<slot name="key"></slot>
|
||||
</div>
|
||||
<div class="value">
|
||||
<div :class="$style.value">
|
||||
<slot name="value"></slot>
|
||||
<button v-if="copy" v-tooltip="i18n.ts.copy" class="_textButton" style="margin-left: 0.5em;" @click="copy_"><i class="ti ti-copy"></i></button>
|
||||
</div>
|
||||
@@ -30,24 +30,18 @@ const copy_ = () => {
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.alqyeyti {
|
||||
> .key {
|
||||
font-size: 0.85em;
|
||||
padding: 0 0 0.25em 0;
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
&.oneline {
|
||||
display: flex;
|
||||
|
||||
> .key {
|
||||
.key {
|
||||
width: 30%;
|
||||
font-size: 1em;
|
||||
padding: 0 8px 0 0;
|
||||
}
|
||||
|
||||
> .value {
|
||||
.value {
|
||||
width: 70%;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
@@ -55,4 +49,10 @@ const copy_ = () => {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.key {
|
||||
font-size: 0.85em;
|
||||
padding: 0 0 0.25em 0;
|
||||
opacity: 0.75;
|
||||
}
|
||||
</style>
|
||||
|
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<MkModal ref="modal" v-slot="{ type, maxHeight }" :prefer-type="preferedModalType" :anchor="anchor" :transparent-bg="true" :src="src" @click="modal.close()" @closed="emit('closed')">
|
||||
<MkModal ref="modal" v-slot="{ type, maxHeight }" :preferType="preferedModalType" :anchor="anchor" :transparentBg="true" :src="src" @click="modal.close()" @closed="emit('closed')">
|
||||
<div class="szkkfdyq _popup _shadow" :class="{ asDrawer: type === 'drawer' }" :style="{ maxHeight: maxHeight ? maxHeight + 'px' : '' }">
|
||||
<div class="main">
|
||||
<template v-for="item in items">
|
||||
|
@@ -8,7 +8,7 @@
|
||||
<ImgWithBlurhash
|
||||
:hash="image.blurhash"
|
||||
:src="(defaultStore.state.enableDataSaverMode && hide) ? null : url"
|
||||
:force-blurhash="hide"
|
||||
:forceBlurhash="hide"
|
||||
:cover="hide"
|
||||
:alt="image.comment || image.name"
|
||||
:title="image.comment || image.name"
|
||||
|
@@ -27,8 +27,8 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import * as misskey from 'misskey-js';
|
||||
import bytes from '@/filters/bytes';
|
||||
import VuePlyr from 'vue-plyr';
|
||||
import bytes from '@/filters/bytes';
|
||||
import { defaultStore } from '@/store';
|
||||
import 'vue-plyr/dist/vue-plyr.css';
|
||||
import { i18n } from '@/i18n';
|
||||
|
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div ref="el" :class="$style.root">
|
||||
<MkMenu :items="items" :align="align" :width="width" :as-drawer="false" @close="onChildClosed"/>
|
||||
<MkMenu :items="items" :align="align" :width="width" :asDrawer="false" @close="onChildClosed"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@@ -50,7 +50,7 @@
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="childMenu" :class="$style.child">
|
||||
<XChild ref="child" :items="childMenu" :target-element="childTarget" :root-element="itemsEl" showing @actioned="childActioned"/>
|
||||
<XChild ref="child" :items="childMenu" :targetElement="childTarget" :rootElement="itemsEl" showing @actioned="childActioned"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
@@ -1,11 +1,11 @@
|
||||
<template>
|
||||
<Transition
|
||||
:name="transitionName"
|
||||
:enter-active-class="$style['transition_' + transitionName + '_enterActive']"
|
||||
:leave-active-class="$style['transition_' + transitionName + '_leaveActive']"
|
||||
:enter-from-class="$style['transition_' + transitionName + '_enterFrom']"
|
||||
:leave-to-class="$style['transition_' + transitionName + '_leaveTo']"
|
||||
:duration="transitionDuration" appear @after-leave="emit('closed')" @enter="emit('opening')" @after-enter="onOpened"
|
||||
:enterActiveClass="$style['transition_' + transitionName + '_enterActive']"
|
||||
:leaveActiveClass="$style['transition_' + transitionName + '_leaveActive']"
|
||||
:enterFromClass="$style['transition_' + transitionName + '_enterFrom']"
|
||||
:leaveToClass="$style['transition_' + transitionName + '_leaveTo']"
|
||||
:duration="transitionDuration" appear @afterLeave="emit('closed')" @enter="emit('opening')" @afterEnter="onOpened"
|
||||
>
|
||||
<div v-show="manualShowing != null ? manualShowing : showing" v-hotkey.global="keymap" :class="[$style.root, { [$style.drawer]: type === 'drawer', [$style.dialog]: type === 'dialog', [$style.popup]: type === 'popup' }]" :style="{ zIndex, pointerEvents: (manualShowing != null ? manualShowing : showing) ? 'auto' : 'none', '--transformOrigin': transformOrigin }">
|
||||
<div data-cy-bg :data-cy-transparent="isEnableBgTransparent" class="_modalBg" :class="[$style.bg, { [$style.bgTransparent]: isEnableBgTransparent }]" :style="{ zIndex }" @click="onBgClick" @mousedown="onBgClick" @contextmenu.prevent.stop="() => {}"></div>
|
||||
|
@@ -55,17 +55,17 @@
|
||||
<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" :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" :emojiUrls="appearNote.emojis"/>
|
||||
<div v-if="translating || translation" :class="$style.translation">
|
||||
<MkLoading v-if="translating" mini/>
|
||||
<div v-else :class="$style.translated">
|
||||
<b>{{ i18n.t('translatedFrom', { x: translation.sourceLang }) }}: </b>
|
||||
<Mfm :text="translation.text" :author="appearNote.user" :i="$i" :emoji-urls="appearNote.emojis"/>
|
||||
<Mfm :text="translation.text" :author="appearNote.user" :i="$i" :emojiUrls="appearNote.emojis"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="appearNote.files.length > 0" :class="$style.files">
|
||||
<MkMediaList :media-list="appearNote.files"/>
|
||||
<MkMediaList :mediaList="appearNote.files"/>
|
||||
</div>
|
||||
<MkPoll v-if="appearNote.poll" :note="appearNote" :class="$style.poll"/>
|
||||
<MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="false" :class="$style.urlPreview"/>
|
||||
@@ -79,7 +79,7 @@
|
||||
</div>
|
||||
<MkA v-if="appearNote.channel && !inChannel" :class="$style.channel" :to="`/channels/${appearNote.channel.id}`"><i class="ti ti-device-tv"></i> {{ appearNote.channel.name }}</MkA>
|
||||
</div>
|
||||
<MkReactionsViewer :note="appearNote" :max-number="16">
|
||||
<MkReactionsViewer :note="appearNote" :maxNumber="16">
|
||||
<template #more>
|
||||
<button class="_button" :class="$style.reactionDetailsButton" @click="showReactions">
|
||||
{{ i18n.ts.more }}
|
||||
|
@@ -65,18 +65,18 @@
|
||||
<div class="text">
|
||||
<span v-if="appearNote.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span>
|
||||
<MkA v-if="appearNote.replyId" class="reply" :to="`/notes/${appearNote.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA>
|
||||
<Mfm v-if="appearNote.text" :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" :emojiUrls="appearNote.emojis"/>
|
||||
<a v-if="appearNote.renote != null" class="rp">RN:</a>
|
||||
<div v-if="translating || translation" class="translation">
|
||||
<MkLoading v-if="translating" mini/>
|
||||
<div v-else class="translated">
|
||||
<b>{{ i18n.t('translatedFrom', { x: translation.sourceLang }) }}: </b>
|
||||
<Mfm :text="translation.text" :author="appearNote.user" :i="$i" :emoji-urls="appearNote.emojis"/>
|
||||
<Mfm :text="translation.text" :author="appearNote.user" :i="$i" :emojiUrls="appearNote.emojis"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="appearNote.files.length > 0" class="files">
|
||||
<MkMediaList :media-list="appearNote.files"/>
|
||||
<MkMediaList :mediaList="appearNote.files"/>
|
||||
</div>
|
||||
<MkPoll v-if="appearNote.poll" ref="pollViewer" :note="appearNote" class="poll"/>
|
||||
<MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="true" class="url-preview"/>
|
||||
|
@@ -5,7 +5,7 @@
|
||||
<MkNoteHeader :class="$style.header" :note="note" :mini="true"/>
|
||||
<div>
|
||||
<p v-if="note.cw != null" :class="$style.cw">
|
||||
<Mfm v-if="note.cw != ''" style="margin-right: 8px;" :text="note.cw" :author="note.user" :i="$i" :emoji-urls="note.emojis"/>
|
||||
<Mfm v-if="note.cw != ''" style="margin-right: 8px;" :text="note.cw" :author="note.user" :i="$i" :emojiUrls="note.emojis"/>
|
||||
<MkCwButton v-model="showContent" :note="note"/>
|
||||
</p>
|
||||
<div v-show="note.cw == null || showContent">
|
||||
|
@@ -15,7 +15,7 @@
|
||||
:items="notes"
|
||||
:direction="pagination.reversed ? 'up' : 'down'"
|
||||
:reversed="pagination.reversed"
|
||||
:no-gap="noGap"
|
||||
:noGap="noGap"
|
||||
:ad="true"
|
||||
:class="$style.notes"
|
||||
>
|
||||
|
@@ -20,8 +20,8 @@
|
||||
v-else-if="notification.type === 'reaction'"
|
||||
ref="reactionRef"
|
||||
:reaction="notification.reaction ? notification.reaction.replace(/^:(\w+):$/, ':$1@.:') : notification.reaction"
|
||||
:custom-emojis="notification.note.emojis"
|
||||
:no-style="true"
|
||||
:customEmojis="notification.note.emojis"
|
||||
:noStyle="true"
|
||||
style="width: 100%; height: 100%;"
|
||||
/>
|
||||
</div>
|
||||
|
@@ -3,15 +3,15 @@
|
||||
ref="dialog"
|
||||
:width="400"
|
||||
:height="450"
|
||||
:with-ok-button="true"
|
||||
:ok-button-disabled="false"
|
||||
:withOkButton="true"
|
||||
:okButtonDisabled="false"
|
||||
@ok="ok()"
|
||||
@close="dialog?.close()"
|
||||
@closed="emit('closed')"
|
||||
>
|
||||
<template #header>{{ i18n.ts.notificationSetting }}</template>
|
||||
|
||||
<MkSpacer :margin-min="20" :margin-max="28">
|
||||
<MkSpacer :marginMin="20" :marginMax="28">
|
||||
<div class="_gaps_m">
|
||||
<template v-if="showGlobalToggle">
|
||||
<MkSwitch v-model="useGlobalSetting">
|
||||
|
@@ -8,9 +8,9 @@
|
||||
</template>
|
||||
|
||||
<template #default="{ items: notifications }">
|
||||
<MkDateSeparatedList v-slot="{ item: notification }" :class="$style.list" :items="notifications" :no-gap="true">
|
||||
<MkDateSeparatedList v-slot="{ item: notification }" :class="$style.list" :items="notifications" :noGap="true">
|
||||
<MkNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :key="notification.id" :note="notification.note"/>
|
||||
<XNotification v-else :key="notification.id" :notification="notification" :with-time="true" :full="true" class="_panel notification"/>
|
||||
<XNotification v-else :key="notification.id" :notification="notification" :withTime="true" :full="true" class="_panel notification"/>
|
||||
</MkDateSeparatedList>
|
||||
</template>
|
||||
</MkPagination>
|
||||
|
@@ -1,12 +1,12 @@
|
||||
<template>
|
||||
<MkWindow
|
||||
ref="windowEl"
|
||||
:initial-width="500"
|
||||
:initial-height="500"
|
||||
:can-resize="true"
|
||||
:close-button="true"
|
||||
:buttons-left="buttonsLeft"
|
||||
:buttons-right="buttonsRight"
|
||||
:initialWidth="500"
|
||||
:initialHeight="500"
|
||||
:canResize="true"
|
||||
:closeButton="true"
|
||||
:buttonsLeft="buttonsLeft"
|
||||
:buttonsRight="buttonsRight"
|
||||
:contextmenu="contextmenu"
|
||||
@closed="$emit('closed')"
|
||||
>
|
||||
|
@@ -1,9 +1,9 @@
|
||||
<template>
|
||||
<Transition
|
||||
:enter-active-class="defaultStore.state.animation ? $style.transition_fade_enterActive : ''"
|
||||
:leave-active-class="defaultStore.state.animation ? $style.transition_fade_leaveActive : ''"
|
||||
:enter-from-class="defaultStore.state.animation ? $style.transition_fade_enterFrom : ''"
|
||||
:leave-to-class="defaultStore.state.animation ? $style.transition_fade_leaveTo : ''"
|
||||
:enterActiveClass="defaultStore.state.animation ? $style.transition_fade_enterActive : ''"
|
||||
:leaveActiveClass="defaultStore.state.animation ? $style.transition_fade_leaveActive : ''"
|
||||
:enterFromClass="defaultStore.state.animation ? $style.transition_fade_enterFrom : ''"
|
||||
:leaveToClass="defaultStore.state.animation ? $style.transition_fade_leaveTo : ''"
|
||||
mode="out-in"
|
||||
>
|
||||
<MkLoading v-if="fetching"/>
|
||||
|
@@ -1,19 +1,19 @@
|
||||
<template>
|
||||
<div class="tivcixzd" :class="{ done: closed || isVoted }">
|
||||
<ul>
|
||||
<li v-for="(choice, i) in note.poll.choices" :key="i" :class="{ voted: choice.voted }" @click="vote(i)">
|
||||
<div class="backdrop" :style="{ 'width': `${showResult ? (choice.votes / total * 100) : 0}%` }"></div>
|
||||
<span>
|
||||
<template v-if="choice.isVoted"><i class="ti ti-check"></i></template>
|
||||
<div :class="{ [$style.done]: closed || isVoted }">
|
||||
<ul :class="$style.choices">
|
||||
<li v-for="(choice, i) in note.poll.choices" :key="i" :class="[$style.choice, { [$style.voted]: choice.voted }]" @click="vote(i)">
|
||||
<div :class="$style.bg" :style="{ 'width': `${showResult ? (choice.votes / total * 100) : 0}%` }"></div>
|
||||
<span :class="$style.fg">
|
||||
<template v-if="choice.isVoted"><i class="ti ti-check" style="margin-right: 4px; color: var(--accent);"></i></template>
|
||||
<Mfm :text="choice.text" :plain="true"/>
|
||||
<span v-if="showResult" class="votes">({{ i18n.t('_poll.votesCount', { n: choice.votes }) }})</span>
|
||||
<span v-if="showResult" style="margin-left: 4px; opacity: 0.7;">({{ i18n.t('_poll.votesCount', { n: choice.votes }) }})</span>
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
<p v-if="!readOnly">
|
||||
<p v-if="!readOnly" :class="$style.info">
|
||||
<span>{{ i18n.t('_poll.totalVotes', { n: total }) }}</span>
|
||||
<span> · </span>
|
||||
<a v-if="!closed && !isVoted" @click="showResult = !showResult">{{ showResult ? i18n.ts._poll.vote : i18n.ts._poll.showResult }}</a>
|
||||
<a v-if="!closed && !isVoted" style="color: inherit;" @click="showResult = !showResult">{{ showResult ? i18n.ts._poll.vote : i18n.ts._poll.showResult }}</a>
|
||||
<span v-if="isVoted">{{ i18n.ts._poll.voted }}</span>
|
||||
<span v-else-if="closed">{{ i18n.ts._poll.closed }}</span>
|
||||
<span v-if="remaining > 0"> · {{ timer }}</span>
|
||||
@@ -86,67 +86,51 @@ const vote = async (id) => {
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tivcixzd {
|
||||
> ul {
|
||||
display: block;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
<style lang="scss" module>
|
||||
.choices {
|
||||
display: block;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
> li {
|
||||
display: block;
|
||||
position: relative;
|
||||
margin: 4px 0;
|
||||
padding: 4px;
|
||||
//border: solid 0.5px var(--divider);
|
||||
background: var(--accentedBg);
|
||||
border-radius: 4px;
|
||||
overflow: clip;
|
||||
cursor: pointer;
|
||||
.choice {
|
||||
display: block;
|
||||
position: relative;
|
||||
margin: 4px 0;
|
||||
padding: 4px;
|
||||
//border: solid 0.5px var(--divider);
|
||||
background: var(--accentedBg);
|
||||
border-radius: 4px;
|
||||
overflow: clip;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
> .backdrop {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
background: var(--accent);
|
||||
background: linear-gradient(90deg,var(--buttonGradateA),var(--buttonGradateB));
|
||||
transition: width 1s ease;
|
||||
}
|
||||
.bg {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
background: var(--accent);
|
||||
background: linear-gradient(90deg,var(--buttonGradateA),var(--buttonGradateB));
|
||||
transition: width 1s ease;
|
||||
}
|
||||
|
||||
> span {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
padding: 3px 5px;
|
||||
background: var(--panel);
|
||||
border-radius: 3px;
|
||||
.fg {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
padding: 3px 5px;
|
||||
background: var(--panel);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
> i {
|
||||
margin-right: 4px;
|
||||
color: var(--accent);
|
||||
}
|
||||
.info {
|
||||
color: var(--fg);
|
||||
}
|
||||
|
||||
> .votes {
|
||||
margin-left: 4px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> p {
|
||||
color: var(--fg);
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
&.done {
|
||||
> ul > li {
|
||||
cursor: default;
|
||||
}
|
||||
.done {
|
||||
.choice {
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@@ -5,7 +5,7 @@
|
||||
</p>
|
||||
<ul>
|
||||
<li v-for="(choice, i) in choices" :key="i">
|
||||
<MkInput class="input" small :model-value="choice" :placeholder="i18n.t('_poll.choiceN', { n: i + 1 })" @update:model-value="onInput(i, $event)">
|
||||
<MkInput class="input" small :modelValue="choice" :placeholder="i18n.t('_poll.choiceN', { n: i + 1 })" @update:modelValue="onInput(i, $event)">
|
||||
</MkInput>
|
||||
<button class="_button" @click="remove(i)">
|
||||
<i class="ti ti-x"></i>
|
||||
|
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<MkModal ref="modal" v-slot="{ type, maxHeight }" :z-priority="'high'" :src="src" :transparent-bg="true" @click="modal.close()" @close="emit('closing')" @closed="emit('closed')">
|
||||
<MkMenu :items="items" :align="align" :width="width" :max-height="maxHeight" :as-drawer="type === 'drawer'" :class="{ [$style.drawer]: type === 'drawer' }" @close="modal.close()"/>
|
||||
<MkModal ref="modal" v-slot="{ type, maxHeight }" :zPriority="'high'" :src="src" :transparentBg="true" @click="modal.close()" @close="emit('closing')" @closed="emit('closed')">
|
||||
<MkMenu :items="items" :align="align" :width="width" :max-height="maxHeight" :asDrawer="type === 'drawer'" :class="{ [$style.drawer]: type === 'drawer' }" @close="modal.close()"/>
|
||||
</MkModal>
|
||||
</template>
|
||||
|
||||
|
@@ -66,7 +66,7 @@
|
||||
<div v-if="maxTextLength - textLength < 100" :class="['_acrylic', $style.textCount, { [$style.textOver]: textLength > maxTextLength }]">{{ maxTextLength - textLength }}</div>
|
||||
</div>
|
||||
<input v-show="withHashtags" ref="hashtagsInputEl" v-model="hashtags" :class="$style.hashtags" :placeholder="i18n.ts.hashtags" list="hashtags">
|
||||
<XPostFormAttaches v-model="files" :class="$style.attaches" @detach="detachFile" @change-sensitive="updateFileSensitive" @change-name="updateFileName"/>
|
||||
<XPostFormAttaches v-model="files" :class="$style.attaches" @detach="detachFile" @changeSensitive="updateFileSensitive" @changeName="updateFileName"/>
|
||||
<MkPollEditor v-if="poll" v-model="poll" @destroyed="poll = null"/>
|
||||
<MkNotePreview v-if="showPreview" :class="$style.preview" :text="text"/>
|
||||
<div v-if="showingOptions" style="padding: 8px 16px;">
|
||||
|
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div v-show="props.modelValue.length != 0" class="skeikyzd">
|
||||
<Sortable :model-value="props.modelValue" class="files" item-key="id" :animation="150" :delay="100" :delay-on-touch-only="true" @update:model-value="v => emit('update:modelValue', v)">
|
||||
<Sortable :modelValue="props.modelValue" class="files" itemKey="id" :animation="150" :delay="100" :delayOnTouchOnly="true" @update:modelValue="v => emit('update:modelValue', v)">
|
||||
<template #item="{element}">
|
||||
<div class="file" @click="showFileMenu(element, $event)" @contextmenu.prevent="showFileMenu(element, $event)">
|
||||
<MkDriveFileThumbnail :data-id="element.id" class="thumbnail" :file="element" fit="cover"/>
|
||||
|
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<MkModal ref="modal" :prefer-type="'dialog'" @click="modal.close()" @closed="onModalClosed()">
|
||||
<MkPostForm ref="form" style="margin: 0 auto auto auto;" v-bind="props" autofocus freeze-after-posted @posted="onPosted" @cancel="modal.close()" @esc="modal.close()"/>
|
||||
<MkModal ref="modal" :preferType="'dialog'" @click="modal.close()" @closed="onModalClosed()">
|
||||
<MkPostForm ref="form" style="margin: 0 auto auto auto;" v-bind="props" autofocus freezeAfterPosted @posted="onPosted" @cancel="modal.close()" @esc="modal.close()"/>
|
||||
</MkModal>
|
||||
</template>
|
||||
|
||||
|
@@ -72,28 +72,28 @@ function subscribe() {
|
||||
userVisibleOnly: true,
|
||||
applicationServerKey: urlBase64ToUint8Array(instance.swPublickey),
|
||||
})
|
||||
.then(async subscription => {
|
||||
pushSubscription = subscription;
|
||||
.then(async subscription => {
|
||||
pushSubscription = subscription;
|
||||
|
||||
// Register
|
||||
pushRegistrationInServer = await api('sw/register', {
|
||||
endpoint: subscription.endpoint,
|
||||
auth: encode(subscription.getKey('auth')),
|
||||
publickey: encode(subscription.getKey('p256dh')),
|
||||
});
|
||||
}, async err => { // When subscribe failed
|
||||
// Register
|
||||
pushRegistrationInServer = await api('sw/register', {
|
||||
endpoint: subscription.endpoint,
|
||||
auth: encode(subscription.getKey('auth')),
|
||||
publickey: encode(subscription.getKey('p256dh')),
|
||||
});
|
||||
}, async err => { // When subscribe failed
|
||||
// 通知が許可されていなかったとき
|
||||
if (err?.name === 'NotAllowedError') {
|
||||
console.info('User denied the notification permission request.');
|
||||
return;
|
||||
}
|
||||
if (err?.name === 'NotAllowedError') {
|
||||
console.info('User denied the notification permission request.');
|
||||
return;
|
||||
}
|
||||
|
||||
// 違うapplicationServerKey (または gcm_sender_id)のサブスクリプションが
|
||||
// 既に存在していることが原因でエラーになった可能性があるので、
|
||||
// そのサブスクリプションを解除しておく
|
||||
// (これは実行されなさそうだけど、おまじない的に古い実装から残してある)
|
||||
await unsubscribe();
|
||||
}), null, null);
|
||||
// 違うapplicationServerKey (または gcm_sender_id)のサブスクリプションが
|
||||
// 既に存在していることが原因でエラーになった可能性があるので、
|
||||
// そのサブスクリプションを解除しておく
|
||||
// (これは実行されなさそうだけど、おまじない的に古い実装から残してある)
|
||||
await unsubscribe();
|
||||
}), null, null);
|
||||
}
|
||||
|
||||
async function unsubscribe() {
|
||||
|
@@ -8,7 +8,7 @@
|
||||
>
|
||||
<template #header>{{ i18n.ts.reactionsList }}</template>
|
||||
|
||||
<MkSpacer :margin-min="20" :margin-max="28">
|
||||
<MkSpacer :marginMin="20" :marginMax="28">
|
||||
<div v-if="note" class="_gaps">
|
||||
<div v-if="reactions.length === 0" class="_fullinfo">
|
||||
<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/>
|
||||
@@ -22,7 +22,7 @@
|
||||
</button>
|
||||
</div>
|
||||
<MkA v-for="user in users" :key="user.id" :to="userPage(user)" @click="dialog.close()">
|
||||
<MkUserCardMini :user="user" :with-chart="false"/>
|
||||
<MkUserCardMini :user="user" :withChart="false"/>
|
||||
</MkA>
|
||||
</template>
|
||||
</div>
|
||||
|
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<MkCustomEmoji v-if="reaction[0] === ':'" :name="reaction" :normal="true" :no-style="noStyle" :url="emojiUrl"/>
|
||||
<MkEmoji v-else :emoji="reaction" :normal="true" :no-style="noStyle"/>
|
||||
<MkCustomEmoji v-if="reaction[0] === ':'" :name="reaction" :normal="true" :noStyle="noStyle" :url="emojiUrl"/>
|
||||
<MkEmoji v-else :emoji="reaction" :normal="true" :noStyle="noStyle"/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<MkTooltip ref="tooltip" :showing="showing" :target-element="targetElement" :max-width="340" @closed="emit('closed')">
|
||||
<MkTooltip ref="tooltip" :showing="showing" :targetElement="targetElement" :maxWidth="340" @closed="emit('closed')">
|
||||
<div :class="$style.root">
|
||||
<MkReactionIcon :reaction="reaction" :class="$style.icon" :no-style="true"/>
|
||||
<MkReactionIcon :reaction="reaction" :class="$style.icon" :noStyle="true"/>
|
||||
<div :class="$style.name">{{ reaction.replace('@.', '') }}</div>
|
||||
</div>
|
||||
</MkTooltip>
|
||||
|
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<MkTooltip ref="tooltip" :showing="showing" :target-element="targetElement" :max-width="340" @closed="emit('closed')">
|
||||
<MkTooltip ref="tooltip" :showing="showing" :targetElement="targetElement" :maxWidth="340" @closed="emit('closed')">
|
||||
<div :class="$style.root">
|
||||
<div :class="$style.reaction">
|
||||
<MkReactionIcon :reaction="reaction" :class="$style.reactionIcon" :no-style="true"/>
|
||||
<MkReactionIcon :reaction="reaction" :class="$style.reactionIcon" :noStyle="true"/>
|
||||
<div :class="$style.reactionName">{{ getReactionName(reaction) }}</div>
|
||||
</div>
|
||||
<div :class="$style.users">
|
||||
|
@@ -1,13 +1,13 @@
|
||||
<template>
|
||||
<TransitionGroup
|
||||
:enter-active-class="defaultStore.state.animation ? $style.transition_x_enterActive : ''"
|
||||
:leave-active-class="defaultStore.state.animation ? $style.transition_x_leaveActive : ''"
|
||||
:enter-from-class="defaultStore.state.animation ? $style.transition_x_enterFrom : ''"
|
||||
:leave-to-class="defaultStore.state.animation ? $style.transition_x_leaveTo : ''"
|
||||
:move-class="defaultStore.state.animation ? $style.transition_x_move : ''"
|
||||
:enterActiveClass="defaultStore.state.animation ? $style.transition_x_enterActive : ''"
|
||||
:leaveActiveClass="defaultStore.state.animation ? $style.transition_x_leaveActive : ''"
|
||||
:enterFromClass="defaultStore.state.animation ? $style.transition_x_enterFrom : ''"
|
||||
:leaveToClass="defaultStore.state.animation ? $style.transition_x_leaveTo : ''"
|
||||
:moveClass="defaultStore.state.animation ? $style.transition_x_move : ''"
|
||||
tag="div" :class="$style.root"
|
||||
>
|
||||
<XReaction v-for="[reaction, count] in reactions" :key="reaction" :reaction="reaction" :count="count" :is-initial="initialReactions.has(reaction)" :note="note"/>
|
||||
<XReaction v-for="[reaction, count] in reactions" :key="reaction" :reaction="reaction" :count="count" :isInitial="initialReactions.has(reaction)" :note="note"/>
|
||||
<slot v-if="hasMoreReactions" name="more"/>
|
||||
</TransitionGroup>
|
||||
</template>
|
||||
|
@@ -8,7 +8,7 @@
|
||||
>
|
||||
<template #header>{{ i18n.ts.renotesList }}</template>
|
||||
|
||||
<MkSpacer :margin-min="20" :margin-max="28">
|
||||
<MkSpacer :marginMin="20" :marginMax="28">
|
||||
<div v-if="renotes" class="_gaps">
|
||||
<div v-if="renotes.length === 0" class="_fullinfo">
|
||||
<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/>
|
||||
@@ -16,7 +16,7 @@
|
||||
</div>
|
||||
<template v-else>
|
||||
<MkA v-for="user in users" :key="user.id" :to="userPage(user)" @click="dialog.close()">
|
||||
<MkUserCardMini :user="user" :with-chart="false"/>
|
||||
<MkUserCardMini :user="user" :withChart="false"/>
|
||||
</MkA>
|
||||
</template>
|
||||
</div>
|
||||
|
@@ -6,11 +6,11 @@
|
||||
{{ message }}
|
||||
</MkInfo>
|
||||
<div v-if="!totpLogin" class="normal-signin _gaps_m">
|
||||
<MkInput v-model="username" :placeholder="i18n.ts.username" type="text" pattern="^[a-zA-Z0-9_]+$" :spellcheck="false" autocomplete="username" autofocus required data-cy-signin-username @update:model-value="onUsernameChange">
|
||||
<MkInput v-model="username" :placeholder="i18n.ts.username" type="text" pattern="^[a-zA-Z0-9_]+$" :spellcheck="false" autocomplete="username" autofocus required data-cy-signin-username @update:modelValue="onUsernameChange">
|
||||
<template #prefix>@</template>
|
||||
<template #suffix>@{{ host }}</template>
|
||||
</MkInput>
|
||||
<MkInput v-if="!user || user && !user.usePasswordLessLogin" v-model="password" :placeholder="i18n.ts.password" type="password" autocomplete="current-password" :with-password-toggle="true" required data-cy-signin-password>
|
||||
<MkInput v-if="!user || user && !user.usePasswordLessLogin" v-model="password" :placeholder="i18n.ts.password" type="password" autocomplete="current-password" :withPasswordToggle="true" required data-cy-signin-password>
|
||||
<template #prefix><i class="ti ti-lock"></i></template>
|
||||
<template #caption><button class="_textButton" type="button" @click="resetPassword">{{ i18n.ts.forgotPassword }}</button></template>
|
||||
</MkInput>
|
||||
@@ -28,7 +28,7 @@
|
||||
</div>
|
||||
<div class="twofa-group totp-group">
|
||||
<p style="margin-bottom:0;">{{ i18n.ts.twoStepAuthentication }}</p>
|
||||
<MkInput v-if="user && user.usePasswordLessLogin" v-model="password" type="password" autocomplete="current-password" :with-password-toggle="true" required>
|
||||
<MkInput v-if="user && user.usePasswordLessLogin" v-model="password" type="password" autocomplete="current-password" :withPasswordToggle="true" required>
|
||||
<template #label>{{ i18n.ts.password }}</template>
|
||||
<template #prefix><i class="ti ti-lock"></i></template>
|
||||
</MkInput>
|
||||
|
@@ -8,8 +8,8 @@
|
||||
>
|
||||
<template #header>{{ i18n.ts.login }}</template>
|
||||
|
||||
<MkSpacer :margin-min="20" :margin-max="28">
|
||||
<MkSignin :auto-set="autoSet" :message="message" @login="onLogin"/>
|
||||
<MkSpacer :marginMin="20" :marginMax="28">
|
||||
<MkSignin :autoSet="autoSet" :message="message" @login="onLogin"/>
|
||||
</MkSpacer>
|
||||
</MkModalWindow>
|
||||
</template>
|
||||
|
@@ -3,13 +3,13 @@
|
||||
<div :class="$style.banner">
|
||||
<i class="ti ti-user-edit"></i>
|
||||
</div>
|
||||
<MkSpacer :margin-min="20" :margin-max="32">
|
||||
<MkSpacer :marginMin="20" :marginMax="32">
|
||||
<form class="_gaps_m" autocomplete="new-password" @submit.prevent="onSubmit">
|
||||
<MkInput v-if="instance.disableRegistration" v-model="invitationCode" type="text" :spellcheck="false" required>
|
||||
<template #label>{{ i18n.ts.invitationCode }}</template>
|
||||
<template #prefix><i class="ti ti-key"></i></template>
|
||||
</MkInput>
|
||||
<MkInput v-model="username" type="text" pattern="^[a-zA-Z0-9_]{1,20}$" :spellcheck="false" required data-cy-signup-username @update:model-value="onChangeUsername">
|
||||
<MkInput v-model="username" type="text" pattern="^[a-zA-Z0-9_]{1,20}$" :spellcheck="false" autocomplete="username" required data-cy-signup-username @update:modelValue="onChangeUsername">
|
||||
<template #label>{{ i18n.ts.username }} <div v-tooltip:dialog="i18n.ts.usernameInfo" class="_button _help"><i class="ti ti-help-circle"></i></div></template>
|
||||
<template #prefix>@</template>
|
||||
<template #suffix>@{{ host }}</template>
|
||||
@@ -24,7 +24,7 @@
|
||||
<span v-else-if="usernameState === 'max-range'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.tooLong }}</span>
|
||||
</template>
|
||||
</MkInput>
|
||||
<MkInput v-if="instance.emailRequiredForSignup" v-model="email" :debounce="true" type="email" :spellcheck="false" required data-cy-signup-email @update:model-value="onChangeEmail">
|
||||
<MkInput v-if="instance.emailRequiredForSignup" v-model="email" :debounce="true" type="email" :spellcheck="false" required data-cy-signup-email @update:modelValue="onChangeEmail">
|
||||
<template #label>{{ i18n.ts.emailAddress }} <div v-tooltip:dialog="i18n.ts._signup.emailAddressInfo" class="_button _help"><i class="ti ti-help-circle"></i></div></template>
|
||||
<template #prefix><i class="ti ti-mail"></i></template>
|
||||
<template #caption>
|
||||
@@ -39,7 +39,7 @@
|
||||
<span v-else-if="emailState === 'error'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.error }}</span>
|
||||
</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="password" type="password" autocomplete="new-password" required data-cy-signup-password @update:model-value="onChangePassword">
|
||||
<MkInput v-model="password" type="password" autocomplete="new-password" required data-cy-signup-password @update:modelValue="onChangePassword">
|
||||
<template #label>{{ i18n.ts.password }}</template>
|
||||
<template #prefix><i class="ti ti-lock"></i></template>
|
||||
<template #caption>
|
||||
@@ -48,7 +48,7 @@
|
||||
<span v-if="passwordStrength == 'high'" style="color: var(--success)"><i class="ti ti-check ti-fw"></i> {{ i18n.ts.strongPassword }}</span>
|
||||
</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="retypedPassword" type="password" autocomplete="new-password" required data-cy-signup-password-retype @update:model-value="onChangePasswordRetype">
|
||||
<MkInput v-model="retypedPassword" type="password" autocomplete="new-password" required data-cy-signup-password-retype @update:modelValue="onChangePasswordRetype">
|
||||
<template #label>{{ i18n.ts.password }} ({{ i18n.ts.retype }})</template>
|
||||
<template #prefix><i class="ti ti-lock"></i></template>
|
||||
<template #caption>
|
||||
|
@@ -3,7 +3,7 @@
|
||||
<div :class="$style.banner">
|
||||
<i class="ti ti-checklist"></i>
|
||||
</div>
|
||||
<MkSpacer :margin-min="20" :margin-max="28">
|
||||
<MkSpacer :marginMin="20" :marginMax="28">
|
||||
<div class="_gaps_m">
|
||||
<div v-if="instance.disableRegistration">
|
||||
<MkInfo warn>{{ i18n.ts.invitationRequiredToRegister }}</MkInfo>
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
<div style="text-align: center;">{{ i18n.ts.pleaseConfirmBelowBeforeSignup }}</div>
|
||||
|
||||
<MkFolder v-if="availableServerRules" :default-open="true">
|
||||
<MkFolder v-if="availableServerRules" :defaultOpen="true">
|
||||
<template #label>{{ i18n.ts.serverRules }}</template>
|
||||
<template #suffix><i v-if="agreeServerRules" class="ti ti-check" style="color: var(--success)"></i></template>
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
<MkSwitch v-model="agreeServerRules" style="margin-top: 16px;">{{ i18n.ts.agree }}</MkSwitch>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder v-if="availableTos" :default-open="true">
|
||||
<MkFolder v-if="availableTos" :defaultOpen="true">
|
||||
<template #label>{{ i18n.ts.termsOfService }}</template>
|
||||
<template #suffix><i v-if="agreeTos" class="ti ti-check" style="color: var(--success)"></i></template>
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
<MkSwitch v-model="agreeTos" style="margin-top: 16px;">{{ i18n.ts.agree }}</MkSwitch>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder :default-open="true">
|
||||
<MkFolder :defaultOpen="true">
|
||||
<template #label>{{ i18n.ts.basicNotesBeforeCreateAccount }}</template>
|
||||
<template #suffix><i v-if="agreeNote" class="ti ti-check" style="color: var(--success)"></i></template>
|
||||
|
||||
|
@@ -11,16 +11,16 @@
|
||||
<div style="overflow-x: clip;">
|
||||
<Transition
|
||||
mode="out-in"
|
||||
:enter-active-class="$style.transition_x_enterActive"
|
||||
:leave-active-class="$style.transition_x_leaveActive"
|
||||
:enter-from-class="$style.transition_x_enterFrom"
|
||||
:leave-to-class="$style.transition_x_leaveTo"
|
||||
:enterActiveClass="$style.transition_x_enterActive"
|
||||
:leaveActiveClass="$style.transition_x_leaveActive"
|
||||
:enterFromClass="$style.transition_x_enterFrom"
|
||||
:leaveToClass="$style.transition_x_leaveTo"
|
||||
>
|
||||
<template v-if="!isAcceptedServerRule">
|
||||
<XServerRules @done="isAcceptedServerRule = true" @cancel="dialog.close()"/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<XSignup :auto-set="autoSet" @signup="onSignup" @signup-email-pending="onSignupEmailPending"/>
|
||||
<XSignup :autoSet="autoSet" @signup="onSignup" @signupEmailPending="onSignupEmailPending"/>
|
||||
</template>
|
||||
</Transition>
|
||||
</div>
|
||||
|
@@ -4,12 +4,12 @@
|
||||
<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" :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" :emojiUrls="note.emojis"/>
|
||||
<MkA v-if="note.renoteId" :class="$style.rp" :to="`/notes/${note.renoteId}`">RN: ...</MkA>
|
||||
</div>
|
||||
<details v-if="note.files.length > 0">
|
||||
<summary>({{ i18n.t('withNFiles', { n: note.files.length }) }})</summary>
|
||||
<MkMediaList :media-list="note.files"/>
|
||||
<MkMediaList :mediaList="note.files"/>
|
||||
</details>
|
||||
<details v-if="note.poll">
|
||||
<summary>{{ i18n.ts.poll }}</summary>
|
||||
|
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<MkNotes ref="tlComponent" :no-gap="!defaultStore.state.showGapBetweenNotesInTimeline" :pagination="pagination" @queue="emit('queue', $event)"/>
|
||||
<MkNotes ref="tlComponent" :noGap="!defaultStore.state.showGapBetweenNotesInTimeline" :pagination="pagination" @queue="emit('queue', $event)"/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
@@ -1,11 +1,11 @@
|
||||
<template>
|
||||
<div>
|
||||
<Transition
|
||||
:enter-active-class="defaultStore.state.animation ? $style.transition_toast_enterActive : ''"
|
||||
:leave-active-class="defaultStore.state.animation ? $style.transition_toast_leaveActive : ''"
|
||||
:enter-from-class="defaultStore.state.animation ? $style.transition_toast_enterFrom : ''"
|
||||
:leave-to-class="defaultStore.state.animation ? $style.transition_toast_leaveTo : ''"
|
||||
appear @after-leave="emit('closed')"
|
||||
:enterActiveClass="defaultStore.state.animation ? $style.transition_toast_enterActive : ''"
|
||||
:leaveActiveClass="defaultStore.state.animation ? $style.transition_toast_leaveActive : ''"
|
||||
:enterFromClass="defaultStore.state.animation ? $style.transition_toast_enterFrom : ''"
|
||||
:leaveToClass="defaultStore.state.animation ? $style.transition_toast_leaveTo : ''"
|
||||
appear @afterLeave="emit('closed')"
|
||||
>
|
||||
<div v-if="showing" class="_acrylic" :class="$style.root" :style="{ zIndex }">
|
||||
<div style="padding: 16px 24px;">
|
||||
|
@@ -3,16 +3,16 @@
|
||||
ref="dialog"
|
||||
:width="400"
|
||||
:height="450"
|
||||
:with-ok-button="true"
|
||||
:ok-button-disabled="false"
|
||||
:can-close="false"
|
||||
:withOkButton="true"
|
||||
:okButtonDisabled="false"
|
||||
:canClose="false"
|
||||
@close="dialog.close()"
|
||||
@closed="$emit('closed')"
|
||||
@ok="ok()"
|
||||
>
|
||||
<template #header>{{ title || i18n.ts.generateAccessToken }}</template>
|
||||
|
||||
<MkSpacer :margin-min="20" :margin-max="28">
|
||||
<MkSpacer :marginMin="20" :marginMax="28">
|
||||
<div class="_gaps_m">
|
||||
<div v-if="information">
|
||||
<MkInfo warn>{{ information }}</MkInfo>
|
||||
|
@@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<Transition
|
||||
:enter-active-class="defaultStore.state.animation ? $style.transition_tooltip_enterActive : ''"
|
||||
:leave-active-class="defaultStore.state.animation ? $style.transition_tooltip_leaveActive : ''"
|
||||
:enter-from-class="defaultStore.state.animation ? $style.transition_tooltip_enterFrom : ''"
|
||||
:leave-to-class="defaultStore.state.animation ? $style.transition_tooltip_leaveTo : ''"
|
||||
appear @after-leave="emit('closed')"
|
||||
:enterActiveClass="defaultStore.state.animation ? $style.transition_tooltip_enterActive : ''"
|
||||
:leaveActiveClass="defaultStore.state.animation ? $style.transition_tooltip_leaveActive : ''"
|
||||
:enterFromClass="defaultStore.state.animation ? $style.transition_tooltip_enterFrom : ''"
|
||||
:leaveToClass="defaultStore.state.animation ? $style.transition_tooltip_leaveTo : ''"
|
||||
appear @afterLeave="emit('closed')"
|
||||
>
|
||||
<div v-show="showing" ref="el" :class="$style.root" class="_acrylic _shadow" :style="{ zIndex, maxWidth: maxWidth + 'px' }">
|
||||
<slot>
|
||||
|
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<MkModal ref="modal" :z-priority="'middle'" @click="$refs.modal.close()" @closed="$emit('closed')">
|
||||
<MkModal ref="modal" :zPriority="'middle'" @click="$refs.modal.close()" @closed="$emit('closed')">
|
||||
<div :class="$style.root">
|
||||
<div :class="$style.title"><MkSparkle>{{ i18n.ts.misskeyUpdated }}</MkSparkle></div>
|
||||
<div :class="$style.version">✨{{ version }}🚀</div>
|
||||
|
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div :class="$style.root" :style="{ zIndex, top: top + 'px', left: left + 'px' }">
|
||||
<Transition :name="defaultStore.state.animation ? '_transition_zoom' : ''" @after-leave="emit('closed')">
|
||||
<Transition :name="defaultStore.state.animation ? '_transition_zoom' : ''" @afterLeave="emit('closed')">
|
||||
<MkUrlPreview v-if="showing" class="_popup _shadow" :url="url"/>
|
||||
</Transition>
|
||||
</div>
|
||||
|
@@ -105,7 +105,7 @@ defineProps<{
|
||||
.mfm {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
@@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<Transition
|
||||
:enter-active-class="defaultStore.state.animation ? $style.transition_popup_enterActive : ''"
|
||||
:leave-active-class="defaultStore.state.animation ? $style.transition_popup_leaveActive : ''"
|
||||
:enter-from-class="defaultStore.state.animation ? $style.transition_popup_enterFrom : ''"
|
||||
:leave-to-class="defaultStore.state.animation ? $style.transition_popup_leaveTo : ''"
|
||||
appear @after-leave="emit('closed')"
|
||||
:enterActiveClass="defaultStore.state.animation ? $style.transition_popup_enterActive : ''"
|
||||
:leaveActiveClass="defaultStore.state.animation ? $style.transition_popup_leaveActive : ''"
|
||||
:enterFromClass="defaultStore.state.animation ? $style.transition_popup_enterFrom : ''"
|
||||
:leaveToClass="defaultStore.state.animation ? $style.transition_popup_leaveTo : ''"
|
||||
appear @afterLeave="emit('closed')"
|
||||
>
|
||||
<div v-if="showing" :class="$style.root" class="_popup _shadow" :style="{ zIndex, top: top + 'px', left: left + 'px' }" @mouseover="() => { emit('mouseover'); }" @mouseleave="() => { emit('mouseleave'); }">
|
||||
<div v-if="user != null">
|
||||
|
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<MkModalWindow
|
||||
ref="dialogEl"
|
||||
:with-ok-button="true"
|
||||
:ok-button-disabled="selected == null"
|
||||
:withOkButton="true"
|
||||
:okButtonDisabled="selected == null"
|
||||
@click="cancel()"
|
||||
@close="cancel()"
|
||||
@ok="ok()"
|
||||
@@ -11,12 +11,12 @@
|
||||
<template #header>{{ i18n.ts.selectUser }}</template>
|
||||
<div :class="$style.root">
|
||||
<div :class="$style.form">
|
||||
<FormSplit :min-width="170">
|
||||
<MkInput v-model="username" :autofocus="true" @update:model-value="search">
|
||||
<FormSplit :minWidth="170">
|
||||
<MkInput v-model="username" :autofocus="true" @update:modelValue="search">
|
||||
<template #label>{{ i18n.ts.username }}</template>
|
||||
<template #prefix>@</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="host" :datalist="[hostname]" @update:model-value="search">
|
||||
<MkInput v-model="host" :datalist="[hostname]" @update:modelValue="search">
|
||||
<template #label>{{ i18n.ts.host }}</template>
|
||||
<template #prefix>@</template>
|
||||
</MkInput>
|
||||
|
@@ -2,7 +2,7 @@
|
||||
<div class="_gaps">
|
||||
<div style="text-align: center;">{{ i18n.ts._initialAccountSetting.followUsers }}</div>
|
||||
|
||||
<MkFolder :default-open="true">
|
||||
<MkFolder :defaultOpen="true">
|
||||
<template #label>{{ i18n.ts.recommended }}</template>
|
||||
|
||||
<MkPagination :pagination="pinnedUsers">
|
||||
@@ -14,7 +14,7 @@
|
||||
</MkPagination>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder :default-open="true">
|
||||
<MkFolder :defaultOpen="true">
|
||||
<template #label>{{ i18n.ts.popularUsers }}</template>
|
||||
|
||||
<MkPagination :pagination="popularUsers">
|
||||
|
@@ -12,11 +12,11 @@
|
||||
</div>
|
||||
</FormSlot>
|
||||
|
||||
<MkInput v-model="name" :max="30" manual-save data-cy-user-setup-user-name>
|
||||
<MkInput v-model="name" :max="30" manualSave data-cy-user-setup-user-name>
|
||||
<template #label>{{ i18n.ts._profile.name }}</template>
|
||||
</MkInput>
|
||||
|
||||
<MkTextarea v-model="description" :max="500" tall manual-save data-cy-user-setup-user-description>
|
||||
<MkTextarea v-model="description" :max="500" tall manualSave data-cy-user-setup-user-description>
|
||||
<template #label>{{ i18n.ts._profile.description }}</template>
|
||||
</MkTextarea>
|
||||
|
||||
|
@@ -20,15 +20,15 @@
|
||||
</div>
|
||||
<Transition
|
||||
mode="out-in"
|
||||
:enter-active-class="$style.transition_x_enterActive"
|
||||
:leave-active-class="$style.transition_x_leaveActive"
|
||||
:enter-from-class="$style.transition_x_enterFrom"
|
||||
:leave-to-class="$style.transition_x_leaveTo"
|
||||
:enterActiveClass="$style.transition_x_enterActive"
|
||||
:leaveActiveClass="$style.transition_x_leaveActive"
|
||||
:enterFromClass="$style.transition_x_enterFrom"
|
||||
:leaveToClass="$style.transition_x_leaveTo"
|
||||
>
|
||||
<template v-if="page === 0">
|
||||
<div :class="$style.centerPage">
|
||||
<MkAnimBg style="position: absolute; top: 0;" :scale="1.5"/>
|
||||
<MkSpacer :margin-min="20" :margin-max="28">
|
||||
<MkSpacer :marginMin="20" :marginMax="28">
|
||||
<div class="_gaps" style="text-align: center;">
|
||||
<i class="ti ti-confetti" style="display: block; margin: auto; font-size: 3em; color: var(--accent);"></i>
|
||||
<div style="font-size: 120%;">{{ i18n.ts._initialAccountSetting.accountCreated }}</div>
|
||||
@@ -40,7 +40,7 @@
|
||||
</template>
|
||||
<template v-else-if="page === 1">
|
||||
<div style="height: 100cqh; overflow: auto;">
|
||||
<MkSpacer :margin-min="20" :margin-max="28">
|
||||
<MkSpacer :marginMin="20" :marginMax="28">
|
||||
<XProfile/>
|
||||
<div class="_buttonsCenter" style="margin-top: 16px;">
|
||||
<MkButton primary rounded gradate data-cy-user-setup-continue @click="page++">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>
|
||||
@@ -50,7 +50,7 @@
|
||||
</template>
|
||||
<template v-else-if="page === 2">
|
||||
<div style="height: 100cqh; overflow: auto;">
|
||||
<MkSpacer :margin-min="20" :margin-max="28">
|
||||
<MkSpacer :marginMin="20" :marginMax="28">
|
||||
<XPrivacy/>
|
||||
<div class="_buttonsCenter" style="margin-top: 16px;">
|
||||
<MkButton primary rounded gradate data-cy-user-setup-continue @click="page++">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>
|
||||
@@ -60,7 +60,7 @@
|
||||
</template>
|
||||
<template v-else-if="page === 3">
|
||||
<div style="height: 100cqh; overflow: auto;">
|
||||
<MkSpacer :margin-min="20" :margin-max="28">
|
||||
<MkSpacer :marginMin="20" :marginMax="28">
|
||||
<XFollow/>
|
||||
</MkSpacer>
|
||||
<div :class="$style.pageFooter">
|
||||
@@ -70,12 +70,12 @@
|
||||
</template>
|
||||
<template v-else-if="page === 4">
|
||||
<div :class="$style.centerPage">
|
||||
<MkSpacer :margin-min="20" :margin-max="28">
|
||||
<MkSpacer :marginMin="20" :marginMax="28">
|
||||
<div class="_gaps" style="text-align: center;">
|
||||
<i class="ti ti-bell-ringing-2" style="display: block; margin: auto; font-size: 3em; color: var(--accent);"></i>
|
||||
<div style="font-size: 120%;">{{ i18n.ts.pushNotification }}</div>
|
||||
<div style="padding: 0 16px;">{{ i18n.t('_initialAccountSetting.pushNotificationDescription', { name: instance.name ?? host }) }}</div>
|
||||
<MkPushNotificationAllowButton primary show-only-to-register style="margin: 0 auto;"/>
|
||||
<MkPushNotificationAllowButton primary showOnlyToRegister style="margin: 0 auto;"/>
|
||||
<MkButton primary rounded gradate style="margin: 16px auto 0 auto;" data-cy-user-setup-continue @click="page++">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
@@ -84,7 +84,7 @@
|
||||
<template v-else-if="page === 5">
|
||||
<div :class="$style.centerPage">
|
||||
<MkAnimBg style="position: absolute; top: 0;" :scale="1.5"/>
|
||||
<MkSpacer :margin-min="20" :margin-max="28">
|
||||
<MkSpacer :marginMin="20" :marginMax="28">
|
||||
<div class="_gaps" style="text-align: center;">
|
||||
<i class="ti ti-check" style="display: block; margin: auto; font-size: 3em; color: var(--accent);"></i>
|
||||
<div style="font-size: 120%;">{{ i18n.ts._initialAccountSetting.initialAccountSettingCompleted }}</div>
|
||||
|
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<MkTooltip ref="tooltip" :showing="showing" :target-element="targetElement" :max-width="250" @closed="emit('closed')">
|
||||
<MkTooltip ref="tooltip" :showing="showing" :targetElement="targetElement" :maxWidth="250" @closed="emit('closed')">
|
||||
<div :class="$style.root">
|
||||
<div v-for="u in users" :key="u.id" :class="$style.user">
|
||||
<MkAvatar :class="$style.avatar" :user="u"/>
|
||||
|
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<MkModal ref="modal" v-slot="{ type }" :z-priority="'high'" :src="src" @click="modal.close()" @closed="emit('closed')">
|
||||
<MkModal ref="modal" v-slot="{ type }" :zPriority="'high'" :src="src" @click="modal.close()" @closed="emit('closed')">
|
||||
<div class="_popup" :class="{ [$style.root]: true, [$style.asDrawer]: type === 'drawer' }">
|
||||
<div :class="[$style.label, $style.item]">
|
||||
{{ i18n.ts.visibility }}
|
||||
|
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<MkModal ref="modal" :prefer-type="'dialog'" :z-priority="'high'" @click="success ? done() : () => {}" @closed="emit('closed')">
|
||||
<MkModal ref="modal" :preferType="'dialog'" :zPriority="'high'" @click="success ? done() : () => {}" @closed="emit('closed')">
|
||||
<div :class="[$style.root, { [$style.iconOnly]: (text == null) || success }]">
|
||||
<i v-if="success" :class="[$style.icon, $style.success]" class="ti ti-check"></i>
|
||||
<MkLoading v-else :class="[$style.icon, $style.waiting]" :em="true"/>
|
||||
|
@@ -10,26 +10,26 @@
|
||||
<MkButton inline @click="$emit('exit')">{{ i18n.ts.close }}</MkButton>
|
||||
</header>
|
||||
<Sortable
|
||||
:model-value="props.widgets"
|
||||
item-key="id"
|
||||
:modelValue="props.widgets"
|
||||
itemKey="id"
|
||||
handle=".handle"
|
||||
:animation="150"
|
||||
:group="{ name: 'SortableMkWidgets' }"
|
||||
:class="$style['edit-editing']"
|
||||
@update:model-value="v => emit('updateWidgets', v)"
|
||||
@update:modelValue="v => emit('updateWidgets', v)"
|
||||
>
|
||||
<template #item="{element}">
|
||||
<div :class="[$style.widget, $style['customize-container']]" data-cy-customize-container>
|
||||
<button :class="$style['customize-container-config']" class="_button" @click.prevent.stop="configWidget(element.id)"><i class="ti ti-settings"></i></button>
|
||||
<button :class="$style['customize-container-remove']" data-cy-customize-container-remove class="_button" @click.prevent.stop="removeWidget(element)"><i class="ti ti-x"></i></button>
|
||||
<div class="handle">
|
||||
<component :is="`widget-${element.name}`" :ref="el => widgetRefs[element.id] = el" class="widget" :class="$style['customize-container-handle-widget']" :widget="element" @update-props="updateWidget(element.id, $event)"/>
|
||||
<component :is="`widget-${element.name}`" :ref="el => widgetRefs[element.id] = el" class="widget" :class="$style['customize-container-handle-widget']" :widget="element" @updateProps="updateWidget(element.id, $event)"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Sortable>
|
||||
</template>
|
||||
<component :is="`widget-${widget.name}`" v-for="widget in widgets" v-else :key="widget.id" :ref="el => widgetRefs[widget.id] = el" :class="$style.widget" :widget="widget" @update-props="updateWidget(widget.id, $event)" @contextmenu.stop="onContextmenu(widget, $event)"/>
|
||||
<component :is="`widget-${widget.name}`" v-for="widget in widgets" v-else :key="widget.id" :ref="el => widgetRefs[widget.id] = el" :class="$style.widget" :widget="widget" @updateProps="updateWidget(widget.id, $event)" @contextmenu.stop="onContextmenu(widget, $event)"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user