Merge branch 'develop'

This commit is contained in:
syuilo 2023-02-05 20:55:51 +09:00
commit baf65bfa69
50 changed files with 347 additions and 86 deletions

View file

@ -16,9 +16,15 @@ files/
misskey-assets/ misskey-assets/
fluent-emojis/ fluent-emojis/
.pnp.* .pnp.*
# .yarn関連
.yarn/* .yarn/*
!.yarn/patches !.yarn/patches
!.yarn/plugins !.yarn/plugins
!.yarn/releases !.yarn/releases
!.yarn/sdks !.yarn/sdks
!.yarn/versions !.yarn/versions
.idea/
packages/*/.vscode/
packages/backend/test/docker-compose.yml

3
.dockleignore Normal file
View file

@ -0,0 +1,3 @@
DKL-DI-0005
DKL-DI-0006
DKL-LI-0003

View file

@ -14,6 +14,8 @@ jobs:
steps: steps:
- name: Check out the repo - name: Check out the repo
uses: actions/checkout@v3.3.0 uses: actions/checkout@v3.3.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2.3.0
- name: Docker meta - name: Docker meta
id: meta id: meta
uses: docker/metadata-action@v4 uses: docker/metadata-action@v4

30
.github/workflows/dockle.yml vendored Normal file
View file

@ -0,0 +1,30 @@
---
name: Dockle
on:
push:
branches:
- master
- develop
pull_request:
jobs:
dockle:
runs-on: ubuntu-latest
env:
DOCKER_CONTENT_TRUST: 1
steps:
- uses: actions/checkout@v3.2.0
- run: |
curl -L -o dockle.deb "https://github.com/goodwithtech/dockle/releases/download/v0.4.10/dockle_0.4.10_Linux-64bit.deb"
sudo dpkg -i dockle.deb
- run: |
cp .config/docker_example.env .config/docker.env
cp ./docker-compose.yml.example ./docker-compose.yml
- run: |
docker compose up -d web
docker tag "$(docker compose images web | awk 'OFS=":" {print $4}' | tail -n +2)" misskey-web:latest
- run: |
cmd="dockle --exit-code 1 misskey-web:latest ${image_name}"
echo "> ${cmd}"
eval "${cmd}"

View file

@ -8,6 +8,23 @@
You should also include the user name that made the change. You should also include the user name that made the change.
--> -->
## 13.4.0 (2023/02/05)
### Improvements
- ロールにアイコンを設定してユーザー名の横に表示できるように
- feat: timeline page for non-login users
- 実績の単なるラッキーの獲得確立を調整
- Add Thai language support
### Bugfixes
- fix(server): 自分のノートをお気に入りに登録しても実績解除される問題を修正
- fix(server): clean up file in FileServer
- fix(server): Deny UNIX domain socket
- fix(server): validate filename and emoji name to improve security
- fix(client): validate input response in aiscript
- fix(client): add webhook delete button
- fix(client): tweak notification style
- fix(client): インラインコードを折り返して表示する
## 13.3.3 (2023/02/04) ## 13.3.3 (2023/02/04)

View file

@ -121,7 +121,7 @@ cp .github/misskey/test.yml .config/
``` ```
Prepare DB/Redis for testing. Prepare DB/Redis for testing.
``` ```
docker-compose -f packages/backend/test/docker-compose.yml up docker compose -f packages/backend/test/docker-compose.yml up
``` ```
Alternatively, prepare an empty (data can be erased) DB and edit `.config/test.yml`. Alternatively, prepare an empty (data can be erased) DB and edit `.config/test.yml`.

View file

@ -8,7 +8,9 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
; echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache \ ; echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache \
&& apt-get update \ && apt-get update \
&& apt-get install -yqq --no-install-recommends \ && apt-get install -yqq --no-install-recommends \
build-essential build-essential wget ca-certificates \
&& wget https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 -O /usr/bin/yq \
&& chmod +x /usr/bin/yq
RUN corepack enable RUN corepack enable
@ -29,6 +31,7 @@ ARG NODE_ENV=production
RUN git submodule update --init RUN git submodule update --init
RUN pnpm build RUN pnpm build
RUN rm -rf .git/
FROM node:${NODE_VERSION}-slim AS runner FROM node:${NODE_VERSION}-slim AS runner
@ -44,11 +47,14 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
ffmpeg tini \ ffmpeg tini \
&& corepack enable \ && corepack enable \
&& groupadd -g "${GID}" misskey \ && groupadd -g "${GID}" misskey \
&& useradd -l -u "${UID}" -g "${GID}" -m -d /misskey misskey && useradd -l -u "${UID}" -g "${GID}" -m -d /misskey misskey \
&& find / -type f -perm /u+s -ignore_readdir_race -exec chmod u-s {} \; \
&& find / -type f -perm /g+s -ignore_readdir_race -exec chmod g-s {} \;
USER misskey USER misskey
WORKDIR /misskey WORKDIR /misskey
COPY --from=builder /usr/bin/yq /usr/bin/yq
COPY --chown=misskey:misskey --from=builder /misskey/node_modules ./node_modules COPY --chown=misskey:misskey --from=builder /misskey/node_modules ./node_modules
COPY --chown=misskey:misskey --from=builder /misskey/built ./built COPY --chown=misskey:misskey --from=builder /misskey/built ./built
COPY --chown=misskey:misskey --from=builder /misskey/packages/backend/node_modules ./packages/backend/node_modules COPY --chown=misskey:misskey --from=builder /misskey/packages/backend/node_modules ./packages/backend/node_modules
@ -58,5 +64,6 @@ COPY --chown=misskey:misskey --from=builder /misskey/fluent-emojis /misskey/flue
COPY --chown=misskey:misskey . ./ COPY --chown=misskey:misskey . ./
ENV NODE_ENV=production ENV NODE_ENV=production
HEALTHCHECK --interval=5s --retries=20 CMD ["/bin/bash", "/misskey/healthcheck.sh"]
ENTRYPOINT ["/usr/bin/tini", "--"] ENTRYPOINT ["/usr/bin/tini", "--"]
CMD ["pnpm", "run", "migrateandstart"] CMD ["pnpm", "run", "migrateandstart"]

4
healthcheck.sh Normal file
View file

@ -0,0 +1,4 @@
#!/bin/bash
PORT=$(yq '.port' /misskey/.config/default.yml)
curl -s -S -o /dev/null "http://localhost:${PORT}"

View file

@ -1195,6 +1195,9 @@ _role:
baseRole: "Rollenvorlage" baseRole: "Rollenvorlage"
useBaseValue: "Wert der Rollenvorlage verwenden" useBaseValue: "Wert der Rollenvorlage verwenden"
chooseRoleToAssign: "Zuzuweisende Rolle auswählen" chooseRoleToAssign: "Zuzuweisende Rolle auswählen"
iconUrl: "Icon-URL"
asBadge: "Als Abzeichen anzeigen"
descriptionOfAsBadge: "Ist dies aktiviert, so wird das Icon dieser Rolle an der Seite der Namen von Benutzern mit dieser Rolle angezeigt."
canEditMembersByModerator: "Moderatoren können Benutzern diese Rolle zuweisen" canEditMembersByModerator: "Moderatoren können Benutzern diese Rolle zuweisen"
descriptionOfCanEditMembersByModerator: "Wenn aktiviert, so können Moderatoren und Adminstratoren anderen Benutzern diese Rolle zuweisen bzw. diese Zuweisung aufheben. Wenn deaktiviert, so ist es nur Administratoren möglich, Zuweisungen dieser Rolle zu verwalten." descriptionOfCanEditMembersByModerator: "Wenn aktiviert, so können Moderatoren und Adminstratoren anderen Benutzern diese Rolle zuweisen bzw. diese Zuweisung aufheben. Wenn deaktiviert, so ist es nur Administratoren möglich, Zuweisungen dieser Rolle zu verwalten."
priority: "Priorität" priority: "Priorität"

View file

@ -1195,6 +1195,9 @@ _role:
baseRole: "Role template" baseRole: "Role template"
useBaseValue: "Use role template value" useBaseValue: "Use role template value"
chooseRoleToAssign: "Select the role to assign" chooseRoleToAssign: "Select the role to assign"
iconUrl: "Icon URL"
asBadge: "Show as badge"
descriptionOfAsBadge: "This role's icon will be displayed next to the username of users with this role if turned on."
canEditMembersByModerator: "Allow moderators to edit the list of members for this role" canEditMembersByModerator: "Allow moderators to edit the list of members for this role"
descriptionOfCanEditMembersByModerator: "When turned on, moderators as well as administrators will be able to assign and unassign users to this role. When turned off, only administrators will be able to assign users." descriptionOfCanEditMembersByModerator: "When turned on, moderators as well as administrators will be able to assign and unassign users to this role. When turned off, only administrators will be able to assign users."
priority: "Priority" priority: "Priority"

View file

@ -1195,6 +1195,9 @@ _role:
baseRole: "Rol base" baseRole: "Rol base"
useBaseValue: "Usar los valores del rol base" useBaseValue: "Usar los valores del rol base"
chooseRoleToAssign: "Selecciona el rol para asignar" chooseRoleToAssign: "Selecciona el rol para asignar"
iconUrl: "URL del ícono"
asBadge: "Mostrar como emblema"
descriptionOfAsBadge: "Este ícono de rol se mostrará a lado del nombre de usuario cuando este rol se encuentre activo."
canEditMembersByModerator: "Permitir a los moderadores editar los miembros" canEditMembersByModerator: "Permitir a los moderadores editar los miembros"
descriptionOfCanEditMembersByModerator: "Si se activa, los moderadores, al igual que los administradores, serán capaces de asignar/quitar usuarios a éste rol. Si se desactiva, sólo los administradores podrán hacerlo." descriptionOfCanEditMembersByModerator: "Si se activa, los moderadores, al igual que los administradores, serán capaces de asignar/quitar usuarios a éste rol. Si se desactiva, sólo los administradores podrán hacerlo."
priority: "Prioridad" priority: "Prioridad"

View file

@ -34,6 +34,7 @@ const languages = [
'pt-PT', 'pt-PT',
'ru-RU', 'ru-RU',
'sk-SK', 'sk-SK',
'th-TH',
'ug-CN', 'ug-CN',
'uk-UA', 'uk-UA',
'vi-VN', 'vi-VN',

View file

@ -1148,7 +1148,7 @@ _achievements:
description: "ここをクリックした" description: "ここをクリックした"
_justPlainLucky: _justPlainLucky:
title: "単なるラッキー" title: "単なるラッキー"
description: "10秒ごとに0.01%の確率で獲得" description: "10秒ごとに0.005%の確率で獲得"
_setNameToSyuilo: _setNameToSyuilo:
title: "神様コンプレックス" title: "神様コンプレックス"
description: "名前を syuilo に設定した" description: "名前を syuilo に設定した"
@ -1184,7 +1184,7 @@ _role:
description: "ロールの説明" description: "ロールの説明"
permission: "ロールの権限" permission: "ロールの権限"
descriptionOfPermission: "<b>モデレーター</b>は基本的なモデレーションに関する操作を行えます。\n<b>管理者</b>はインスタンスの全ての設定を変更できます。" descriptionOfPermission: "<b>モデレーター</b>は基本的なモデレーションに関する操作を行えます。\n<b>管理者</b>はインスタンスの全ての設定を変更できます。"
assignTarget: "アサインターゲット" assignTarget: "アサイン"
descriptionOfAssignTarget: "<b>マニュアル</b>は誰がこのロールに含まれるかを手動で管理します。\n<b>コンディショナル</b>は条件を設定し、それに合致するユーザーが自動で含まれるようになります。" descriptionOfAssignTarget: "<b>マニュアル</b>は誰がこのロールに含まれるかを手動で管理します。\n<b>コンディショナル</b>は条件を設定し、それに合致するユーザーが自動で含まれるようになります。"
manual: "マニュアル" manual: "マニュアル"
conditional: "コンディショナル" conditional: "コンディショナル"
@ -1197,6 +1197,9 @@ _role:
baseRole: "ベースロール" baseRole: "ベースロール"
useBaseValue: "ベースロールの値を使用" useBaseValue: "ベースロールの値を使用"
chooseRoleToAssign: "アサインするロールを選択" chooseRoleToAssign: "アサインするロールを選択"
iconUrl: "アイコン画像のURL"
asBadge: "バッジとして表示"
descriptionOfAsBadge: "オンにすると、ユーザー名の横にロールのアイコンが表示されます。"
canEditMembersByModerator: "モデレーターのメンバー編集を許可" canEditMembersByModerator: "モデレーターのメンバー編集を許可"
descriptionOfCanEditMembersByModerator: "オンにすると、管理者に加えてモデレーターもこのロールへユーザーをアサイン/アサイン解除できるようになります。オフにすると管理者のみが行えます。" descriptionOfCanEditMembersByModerator: "オンにすると、管理者に加えてモデレーターもこのロールへユーザーをアサイン/アサイン解除できるようになります。オフにすると管理者のみが行えます。"
priority: "優先度" priority: "優先度"

2
locales/lo-LA.yml Normal file
View file

@ -0,0 +1,2 @@
---
_lang_: "ພາສາລາວ"

View file

@ -1195,6 +1195,9 @@ _role:
baseRole: "บทบาทพื้นฐาน" baseRole: "บทบาทพื้นฐาน"
useBaseValue: "ใช้บทบาทพื้นฐานเริ่มต้น" useBaseValue: "ใช้บทบาทพื้นฐานเริ่มต้น"
chooseRoleToAssign: "เลือกบทบาทที่ต้องการกำหนด" chooseRoleToAssign: "เลือกบทบาทที่ต้องการกำหนด"
iconUrl: "ไอคอน URL"
asBadge: "แสดงเป็นตรา"
descriptionOfAsBadge: "ไอคอนของบทบาทนี้จะปรากฏถัดจากชื่อผู้ใช้ของผู้ใช้งานด้วยบทบาทนี้ถ้าหากเปิดใช้งาน"
canEditMembersByModerator: "อนุญาตให้ผู้ดูแลแก้ไขสมาชิก" canEditMembersByModerator: "อนุญาตให้ผู้ดูแลแก้ไขสมาชิก"
descriptionOfCanEditMembersByModerator: "เมื่อเปิดใช้ ผู้ดูแลนอกเหนือจากผู้ดูแลระบบแล้ว จะสามารถกำหนดและยกเลิกการมอบหมายบทบาทนี้ให้กับผู้ใช้ได้ เมื่อปิด เฉพาะผู้ดูแลระบบเท่านั้นที่จะสามารถกำหนดผู้ใช้ได้นะ" descriptionOfCanEditMembersByModerator: "เมื่อเปิดใช้ ผู้ดูแลนอกเหนือจากผู้ดูแลระบบแล้ว จะสามารถกำหนดและยกเลิกการมอบหมายบทบาทนี้ให้กับผู้ใช้ได้ เมื่อปิด เฉพาะผู้ดูแลระบบเท่านั้นที่จะสามารถกำหนดผู้ใช้ได้นะ"
priority: "ลำดับความสำคัญ" priority: "ลำดับความสำคัญ"

View file

@ -1195,6 +1195,9 @@ _role:
baseRole: "基本角色" baseRole: "基本角色"
useBaseValue: "使用基本角色的值" useBaseValue: "使用基本角色的值"
chooseRoleToAssign: "选择要分配的角色" chooseRoleToAssign: "选择要分配的角色"
iconUrl: "图标URL"
asBadge: "作为徽章显示"
descriptionOfAsBadge: "开启后,用户名旁边将会出现角色图标。"
canEditMembersByModerator: "允许监察者编辑成员" canEditMembersByModerator: "允许监察者编辑成员"
descriptionOfCanEditMembersByModerator: "如果选中,监察者和管理员都能够为用户分配/取消分配角色。如果未选中,则只有管理员可以执行此操作。" descriptionOfCanEditMembersByModerator: "如果选中,监察者和管理员都能够为用户分配/取消分配角色。如果未选中,则只有管理员可以执行此操作。"
priority: "优先级" priority: "优先级"

View file

@ -1195,6 +1195,9 @@ _role:
baseRole: "基本角色" baseRole: "基本角色"
useBaseValue: "使用基本角色的值" useBaseValue: "使用基本角色的值"
chooseRoleToAssign: "選擇要指派的角色" chooseRoleToAssign: "選擇要指派的角色"
iconUrl: "圖示的URL"
asBadge: "顯示為徽章"
descriptionOfAsBadge: "開啟的話,角色圖示會顯示在用戶名旁邊。"
canEditMembersByModerator: "允許編輯監察員的成員" canEditMembersByModerator: "允許編輯監察員的成員"
descriptionOfCanEditMembersByModerator: "如果開啟,管理員與監察員都可以為使用者指派/解除指派該角色。如果關閉,則只有管理員可以執行。" descriptionOfCanEditMembersByModerator: "如果開啟,管理員與監察員都可以為使用者指派/解除指派該角色。如果關閉,則只有管理員可以執行。"
priority: "優先級" priority: "優先級"

View file

@ -1,6 +1,6 @@
{ {
"name": "misskey", "name": "misskey",
"version": "13.3.4", "version": "13.4.0",
"codename": "nasubi", "codename": "nasubi",
"repository": { "repository": {
"type": "git", "type": "git",

View file

@ -0,0 +1,13 @@
export class roleIconBadge1675557528704 {
name = 'roleIconBadge1675557528704'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "role" ADD "iconUrl" character varying(512)`);
await queryRunner.query(`ALTER TABLE "role" ADD "asBadge" boolean NOT NULL DEFAULT false`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "role" DROP COLUMN "asBadge"`);
await queryRunner.query(`ALTER TABLE "role" DROP COLUMN "iconUrl"`);
}
}

View file

@ -60,6 +60,7 @@ export class DownloadService {
retry: { retry: {
limit: 0, limit: 0,
}, },
enableUnixSockets: false,
}).on('response', (res: Got.Response) => { }).on('response', (res: Got.Response) => {
if ((process.env.NODE_ENV === 'production' || process.env.NODE_ENV === 'test') && !this.config.proxy && res.ip) { if ((process.env.NODE_ENV === 'production' || process.env.NODE_ENV === 'test') && !this.config.proxy && res.ip) {
if (this.isPrivateIp(res.ip)) { if (this.isPrivateIp(res.ip)) {

View file

@ -202,6 +202,19 @@ export class RoleService implements OnApplicationShutdown {
return [...assignedRoles, ...matchedCondRoles]; return [...assignedRoles, ...matchedCondRoles];
} }
/**
*
*/
@bindThis
public async getUserBadgeRoles(userId: User['id']) {
const assigns = await this.roleAssignmentByUserIdCache.fetch(userId, () => this.roleAssignmentsRepository.findBy({ userId }));
const assignedRoleIds = assigns.map(x => x.roleId);
const roles = await this.rolesCache.fetch(null, () => this.rolesRepository.findBy({}));
const assignedBadgeRoles = roles.filter(r => r.asBadge && assignedRoleIds.includes(r.id));
// コンディショナルロールも含めるのは負荷高そうだから一旦無し
return assignedBadgeRoles;
}
@bindThis @bindThis
public async getUserPolicies(userId: User['id'] | null): Promise<RolePolicies> { public async getUserPolicies(userId: User['id'] | null): Promise<RolePolicies> {
const meta = await this.metaService.fetch(); const meta = await this.metaService.fetch();

View file

@ -56,11 +56,13 @@ export class RoleEntityService {
name: role.name, name: role.name,
description: role.description, description: role.description,
color: role.color, color: role.color,
iconUrl: role.iconUrl,
target: role.target, target: role.target,
condFormula: role.condFormula, condFormula: role.condFormula,
isPublic: role.isPublic, isPublic: role.isPublic,
isAdministrator: role.isAdministrator, isAdministrator: role.isAdministrator,
isModerator: role.isModerator, isModerator: role.isModerator,
asBadge: role.asBadge,
canEditMembersByModerator: role.canEditMembersByModerator, canEditMembersByModerator: role.canEditMembersByModerator,
policies: policies, policies: policies,
usersCount: assigns.length, usersCount: assigns.length,

View file

@ -415,6 +415,11 @@ export class UserEntityService implements OnModuleInit {
} : undefined) : undefined, } : undefined) : undefined,
emojis: this.customEmojiService.populateEmojis(user.emojis, user.host), emojis: this.customEmojiService.populateEmojis(user.emojis, user.host),
onlineStatus: this.getOnlineStatus(user), onlineStatus: this.getOnlineStatus(user),
// パフォーマンス上の理由でローカルユーザーのみ
badgeRoles: user.host == null ? this.roleService.getUserBadgeRoles(user.id).then(rs => rs.map(r => ({
name: r.name,
iconUrl: r.iconUrl,
}))) : undefined,
...(opts.detail ? { ...(opts.detail ? {
url: profile!.url, url: profile!.url,
@ -454,6 +459,7 @@ export class UserEntityService implements OnModuleInit {
id: role.id, id: role.id,
name: role.name, name: role.name,
color: role.color, color: role.color,
iconUrl: role.iconUrl,
description: role.description, description: role.description,
isModerator: role.isModerator, isModerator: role.isModerator,
isAdministrator: role.isAdministrator, isAdministrator: role.isAdministrator,

View file

@ -102,6 +102,11 @@ export class Role {
}) })
public color: string | null; public color: string | null;
@Column('varchar', {
length: 512, nullable: true,
})
public iconUrl: string | null;
@Column('enum', { @Column('enum', {
enum: ['manual', 'conditional'], enum: ['manual', 'conditional'],
default: 'manual', default: 'manual',
@ -118,6 +123,12 @@ export class Role {
}) })
public isPublic: boolean; public isPublic: boolean;
// trueの場合ユーザー名の横にバッジとして表示
@Column('boolean', {
default: false,
})
public asBadge: boolean;
@Column('boolean', { @Column('boolean', {
default: false, default: false,
}) })

View file

@ -12,9 +12,9 @@ import type Logger from '@/logger.js';
import { DriveService } from '@/core/DriveService.js'; import { DriveService } from '@/core/DriveService.js';
import { createTemp, createTempDir } from '@/misc/create-temp.js'; import { createTemp, createTempDir } from '@/misc/create-temp.js';
import { DownloadService } from '@/core/DownloadService.js'; import { DownloadService } from '@/core/DownloadService.js';
import { bindThis } from '@/decorators.js';
import { QueueLoggerService } from '../QueueLoggerService.js'; import { QueueLoggerService } from '../QueueLoggerService.js';
import type Bull from 'bull'; import type Bull from 'bull';
import { bindThis } from '@/decorators.js';
@Injectable() @Injectable()
export class ExportCustomEmojisProcessorService { export class ExportCustomEmojisProcessorService {
@ -82,6 +82,10 @@ export class ExportCustomEmojisProcessorService {
}); });
for (const emoji of customEmojis) { for (const emoji of customEmojis) {
if (!/^[a-zA-Z0-9_]+$/.test(emoji.name)) {
this.logger.error(`invalid emoji name: ${emoji.name}`);
continue;
}
const ext = mime.extension(emoji.type ?? 'image/png'); const ext = mime.extension(emoji.type ?? 'image/png');
const fileName = emoji.name + (ext ? '.' + ext : ''); const fileName = emoji.name + (ext ? '.' + ext : '');
const emojiPath = path + '/' + fileName; const emojiPath = path + '/' + fileName;

View file

@ -81,6 +81,10 @@ export class ImportCustomEmojisProcessorService {
for (const record of meta.emojis) { for (const record of meta.emojis) {
if (!record.downloaded) continue; if (!record.downloaded) continue;
if (!/^[a-zA-Z0-9_]+?([a-zA-Z0-9\.]+)?$/.test(record.fileName)) {
this.logger.error(`invalid filename: ${record.fileName}`);
continue;
}
const emojiInfo = record.emoji; const emojiInfo = record.emoji;
const emojiPath = outputPath + '/' + record.fileName; const emojiPath = outputPath + '/' + record.fileName;
await this.emojisRepository.delete({ await this.emojisRepository.delete({

View file

@ -146,6 +146,8 @@ export class FileServerService {
const url = new URL(`${this.config.mediaProxy}/static.webp`); const url = new URL(`${this.config.mediaProxy}/static.webp`);
url.searchParams.set('url', file.url); url.searchParams.set('url', file.url);
url.searchParams.set('static', '1'); url.searchParams.set('static', '1');
file.cleanup();
return await reply.redirect(301, url.toString()); return await reply.redirect(301, url.toString());
} else if (file.mime.startsWith('video/')) { } else if (file.mime.startsWith('video/')) {
image = await this.videoProcessingService.generateVideoThumbnail(file.path); image = await this.videoProcessingService.generateVideoThumbnail(file.path);
@ -158,6 +160,8 @@ export class FileServerService {
const url = new URL(`${this.config.mediaProxy}/svg.webp`); const url = new URL(`${this.config.mediaProxy}/svg.webp`);
url.searchParams.set('url', file.url); url.searchParams.set('url', file.url);
file.cleanup();
return await reply.redirect(301, url.toString()); return await reply.redirect(301, url.toString());
} }
} }

View file

@ -19,11 +19,13 @@ export const paramDef = {
name: { type: 'string' }, name: { type: 'string' },
description: { type: 'string' }, description: { type: 'string' },
color: { type: 'string', nullable: true }, color: { type: 'string', nullable: true },
iconUrl: { type: 'string', nullable: true },
target: { type: 'string' }, target: { type: 'string' },
condFormula: { type: 'object' }, condFormula: { type: 'object' },
isPublic: { type: 'boolean' }, isPublic: { type: 'boolean' },
isModerator: { type: 'boolean' }, isModerator: { type: 'boolean' },
isAdministrator: { type: 'boolean' }, isAdministrator: { type: 'boolean' },
asBadge: { type: 'boolean' },
canEditMembersByModerator: { type: 'boolean' }, canEditMembersByModerator: { type: 'boolean' },
policies: { policies: {
type: 'object', type: 'object',
@ -33,11 +35,13 @@ export const paramDef = {
'name', 'name',
'description', 'description',
'color', 'color',
'iconUrl',
'target', 'target',
'condFormula', 'condFormula',
'isPublic', 'isPublic',
'isModerator', 'isModerator',
'isAdministrator', 'isAdministrator',
'asBadge',
'canEditMembersByModerator', 'canEditMembersByModerator',
'policies', 'policies',
], ],
@ -64,11 +68,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
name: ps.name, name: ps.name,
description: ps.description, description: ps.description,
color: ps.color, color: ps.color,
iconUrl: ps.iconUrl,
target: ps.target, target: ps.target,
condFormula: ps.condFormula, condFormula: ps.condFormula,
isPublic: ps.isPublic, isPublic: ps.isPublic,
isAdministrator: ps.isAdministrator, isAdministrator: ps.isAdministrator,
isModerator: ps.isModerator, isModerator: ps.isModerator,
asBadge: ps.asBadge,
canEditMembersByModerator: ps.canEditMembersByModerator, canEditMembersByModerator: ps.canEditMembersByModerator,
policies: ps.policies, policies: ps.policies,
}).then(x => this.rolesRepository.findOneByOrFail(x.identifiers[0])); }).then(x => this.rolesRepository.findOneByOrFail(x.identifiers[0]));

View file

@ -27,11 +27,13 @@ export const paramDef = {
name: { type: 'string' }, name: { type: 'string' },
description: { type: 'string' }, description: { type: 'string' },
color: { type: 'string', nullable: true }, color: { type: 'string', nullable: true },
iconUrl: { type: 'string', nullable: true },
target: { type: 'string' }, target: { type: 'string' },
condFormula: { type: 'object' }, condFormula: { type: 'object' },
isPublic: { type: 'boolean' }, isPublic: { type: 'boolean' },
isModerator: { type: 'boolean' }, isModerator: { type: 'boolean' },
isAdministrator: { type: 'boolean' }, isAdministrator: { type: 'boolean' },
asBadge: { type: 'boolean' },
canEditMembersByModerator: { type: 'boolean' }, canEditMembersByModerator: { type: 'boolean' },
policies: { policies: {
type: 'object', type: 'object',
@ -42,11 +44,13 @@ export const paramDef = {
'name', 'name',
'description', 'description',
'color', 'color',
'iconUrl',
'target', 'target',
'condFormula', 'condFormula',
'isPublic', 'isPublic',
'isModerator', 'isModerator',
'isAdministrator', 'isAdministrator',
'asBadge',
'canEditMembersByModerator', 'canEditMembersByModerator',
'policies', 'policies',
], ],
@ -73,11 +77,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
name: ps.name, name: ps.name,
description: ps.description, description: ps.description,
color: ps.color, color: ps.color,
iconUrl: ps.iconUrl,
target: ps.target, target: ps.target,
condFormula: ps.condFormula, condFormula: ps.condFormula,
isPublic: ps.isPublic, isPublic: ps.isPublic,
isModerator: ps.isModerator, isModerator: ps.isModerator,
isAdministrator: ps.isAdministrator, isAdministrator: ps.isAdministrator,
asBadge: ps.asBadge,
canEditMembersByModerator: ps.canEditMembersByModerator, canEditMembersByModerator: ps.canEditMembersByModerator,
policies: ps.policies, policies: ps.policies,
}); });

View file

@ -5,8 +5,8 @@ import { IdService } from '@/core/IdService.js';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import { GetterService } from '@/server/api/GetterService.js'; import { GetterService } from '@/server/api/GetterService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { ApiError } from '../../../error.js';
import { AchievementService } from '@/core/AchievementService.js'; import { AchievementService } from '@/core/AchievementService.js';
import { ApiError } from '../../../error.js';
export const meta = { export const meta = {
tags: ['notes', 'favorites'], tags: ['notes', 'favorites'],
@ -79,7 +79,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
userId: me.id, userId: me.id,
}); });
if (note.userHost == null) { if (note.userHost == null && note.userId !== me.id) {
this.achievementService.create(note.userId, 'myNoteFavorited1'); this.achievementService.create(note.userId, 'myNoteFavorited1');
} }
}); });

View file

@ -1,6 +1,6 @@
<!-- eslint-disable vue/no-v-html --> <!-- eslint-disable vue/no-v-html -->
<template> <template>
<code v-if="inline" :class="`language-${prismLang}`" v-html="html"></code> <code v-if="inline" :class="`language-${prismLang}`" style="overflow-wrap: anywhere;" v-html="html"></code>
<pre v-else :class="`language-${prismLang}`"><code :class="`language-${prismLang}`" v-html="html"></code></pre> <pre v-else :class="`language-${prismLang}`"><code :class="`language-${prismLang}`" v-html="html"></code></pre>
</template> </template>

View file

@ -107,19 +107,19 @@ export default defineComponent({
return () => h( return () => h(
defaultStore.state.animation ? TransitionGroup : 'div', defaultStore.state.animation ? TransitionGroup : 'div',
{ {
class: { class: {
[$style['date-separated-list']]: true, [$style['date-separated-list']]: true,
[$style['date-separated-list-nogap']]: props.noGap, [$style['date-separated-list-nogap']]: props.noGap,
[$style['reversed']]: props.reversed, [$style['reversed']]: props.reversed,
[$style['direction-down']]: props.direction === 'down', [$style['direction-down']]: props.direction === 'down',
[$style['direction-up']]: props.direction === 'up', [$style['direction-up']]: props.direction === 'up',
}, },
...(defaultStore.state.animation ? { ...(defaultStore.state.animation ? {
name: 'list', name: 'list',
tag: 'div', tag: 'div',
onBeforeLeave, onBeforeLeave,
onLeaveCanceled, onLeaveCanceled,
} : {}), } : {}),
}, },
{ default: renderChildren }); { default: renderChildren });
}, },
@ -139,18 +139,10 @@ export default defineComponent({
transition: none !important; transition: none !important;
} }
> .list-leave-active,
> .list-enter-active { > .list-enter-active {
transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1), opacity 0.7s cubic-bezier(0.23, 1, 0.32, 1); transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1), opacity 0.7s cubic-bezier(0.23, 1, 0.32, 1);
} }
> .list-leave-from,
> .list-leave-to,
> .list-leave-active {
transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1), opacity 0.7s cubic-bezier(0.23, 1, 0.32, 1);
position: absolute !important;
}
> *:empty { > *:empty {
display: none; display: none;
} }

View file

@ -5,6 +5,9 @@
</MkA> </MkA>
<div v-if="note.user.isBot" :class="$style.isBot">bot</div> <div v-if="note.user.isBot" :class="$style.isBot">bot</div>
<div :class="$style.username"><MkAcct :user="note.user"/></div> <div :class="$style.username"><MkAcct :user="note.user"/></div>
<div v-if="note.user.badgeRoles" :class="$style.badgeRoles">
<img v-for="role in note.user.badgeRoles" :key="role.id" v-tooltip="role.name" :class="$style.badgeRole" :src="role.iconUrl"/>
</div>
<div :class="$style.info"> <div :class="$style.info">
<MkA :to="notePage(note)"> <MkA :to="notePage(note)">
<MkTime :time="note.createdAt"/> <MkTime :time="note.createdAt"/>
@ -77,4 +80,17 @@ defineProps<{
margin-left: auto; margin-left: auto;
font-size: 0.9em; font-size: 0.9em;
} }
.badgeRoles {
margin: 0 .5em 0 0;
}
.badgeRole {
height: 1.3em;
vertical-align: -20%;
& + .badgeRole {
margin-left: .125em;
}
}
</style> </style>

View file

@ -63,10 +63,23 @@
<MkA v-else-if="notification.type === 'achievementEarned'" :class="$style.text" to="/my/achievements"> <MkA v-else-if="notification.type === 'achievementEarned'" :class="$style.text" to="/my/achievements">
{{ i18n.ts._achievements._types['_' + notification.achievement].title }} {{ i18n.ts._achievements._types['_' + notification.achievement].title }}
</MkA> </MkA>
<span v-else-if="notification.type === 'follow'" :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.youGotNewFollower }}<div v-if="full"><MkFollowButton :user="notification.user" :full="true"/></div></span> <template v-else-if="notification.type === 'follow'">
<span :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.youGotNewFollower }}</span>
<div v-if="full"><MkFollowButton :user="notification.user" :full="true"/></div>
</template>
<span v-else-if="notification.type === 'followRequestAccepted'" :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.followRequestAccepted }}</span> <span v-else-if="notification.type === 'followRequestAccepted'" :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.followRequestAccepted }}</span>
<span v-else-if="notification.type === 'receiveFollowRequest'" :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.receiveFollowRequest }}<div v-if="full && !followRequestDone"><button class="_textButton" @click="acceptFollowRequest()">{{ i18n.ts.accept }}</button> | <button class="_textButton" @click="rejectFollowRequest()">{{ i18n.ts.reject }}</button></div></span> <template v-else-if="notification.type === 'receiveFollowRequest'">
<span v-else-if="notification.type === 'groupInvited'" :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.groupInvited }}: <b>{{ notification.invitation.group.name }}</b><div v-if="full && !groupInviteDone"><button class="_textButton" @click="acceptGroupInvitation()">{{ i18n.ts.accept }}</button> | <button class="_textButton" @click="rejectGroupInvitation()">{{ i18n.ts.reject }}</button></div></span> <span :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.receiveFollowRequest }}</span>
<div v-if="full && !followRequestDone">
<button class="_textButton" @click="acceptFollowRequest()">{{ i18n.ts.accept }}</button> | <button class="_textButton" @click="rejectFollowRequest()">{{ i18n.ts.reject }}</button>
</div>
</template>
<template v-else-if="notification.type === 'groupInvited'">
<span :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.groupInvited }}: <b>{{ notification.invitation.group.name }}</b></span>
<div v-if="full && !groupInviteDone">
<button class="_textButton" @click="acceptGroupInvitation()">{{ i18n.ts.accept }}</button> | <button class="_textButton" @click="rejectGroupInvitation()">{{ i18n.ts.reject }}</button>
</div>
</template>
<span v-else-if="notification.type === 'app'" :class="$style.text"> <span v-else-if="notification.type === 'app'" :class="$style.text">
<Mfm :text="notification.body" :nowrap="false"/> <Mfm :text="notification.body" :nowrap="false"/>
</span> </span>

View file

@ -438,7 +438,7 @@ if ($i) {
} }
window.setInterval(() => { window.setInterval(() => {
if (Math.floor(Math.random() * 10000) === 0) { if (Math.floor(Math.random() * 20000) === 0) {
claimAchievement('justPlainLucky'); claimAchievement('justPlainLucky');
} }
}, 1000 * 10); }, 1000 * 10);

View file

@ -13,6 +13,10 @@
<template #caption>#RRGGBB</template> <template #caption>#RRGGBB</template>
</MkInput> </MkInput>
<MkInput v-model="iconUrl">
<template #label>{{ i18n.ts._role.iconUrl }}</template>
</MkInput>
<MkSelect v-model="rolePermission" :readonly="readonly"> <MkSelect v-model="rolePermission" :readonly="readonly">
<template #label><i class="ti ti-shield-lock"></i> {{ i18n.ts._role.permission }}</template> <template #label><i class="ti ti-shield-lock"></i> {{ i18n.ts._role.permission }}</template>
<template #caption><div v-html="i18n.ts._role.descriptionOfPermission.replaceAll('\n', '<br>')"></div></template> <template #caption><div v-html="i18n.ts._role.descriptionOfPermission.replaceAll('\n', '<br>')"></div></template>
@ -35,6 +39,21 @@
</div> </div>
</MkFolder> </MkFolder>
<MkSwitch v-model="canEditMembersByModerator" :readonly="readonly">
<template #label>{{ i18n.ts._role.canEditMembersByModerator }}</template>
<template #caption>{{ i18n.ts._role.descriptionOfCanEditMembersByModerator }}</template>
</MkSwitch>
<MkSwitch v-model="isPublic" :readonly="readonly">
<template #label>{{ i18n.ts._role.isPublic }}</template>
<template #caption>{{ i18n.ts._role.descriptionOfIsPublic }}</template>
</MkSwitch>
<MkSwitch v-model="asBadge" :readonly="readonly">
<template #label>{{ i18n.ts._role.asBadge }}</template>
<template #caption>{{ i18n.ts._role.descriptionOfAsBadge }}</template>
</MkSwitch>
<FormSlot> <FormSlot>
<template #label><i class="ti ti-license"></i> {{ i18n.ts._role.policies }}</template> <template #label><i class="ti ti-license"></i> {{ i18n.ts._role.policies }}</template>
<div class="_gaps_s"> <div class="_gaps_s">
@ -358,16 +377,6 @@
</div> </div>
</FormSlot> </FormSlot>
<MkSwitch v-model="canEditMembersByModerator" :readonly="readonly">
<template #label>{{ i18n.ts._role.canEditMembersByModerator }}</template>
<template #caption>{{ i18n.ts._role.descriptionOfCanEditMembersByModerator }}</template>
</MkSwitch>
<MkSwitch v-model="isPublic" :readonly="readonly">
<template #label>{{ i18n.ts._role.isPublic }}</template>
<template #caption>{{ i18n.ts._role.descriptionOfIsPublic }}</template>
</MkSwitch>
<div v-if="!readonly" class="_buttons"> <div v-if="!readonly" class="_buttons">
<MkButton primary rounded @click="save"><i class="ti ti-check"></i> {{ role ? i18n.ts.save : i18n.ts.create }}</MkButton> <MkButton primary rounded @click="save"><i class="ti ti-check"></i> {{ role ? i18n.ts.save : i18n.ts.create }}</MkButton>
</div> </div>
@ -426,9 +435,11 @@ let name = $ref(role?.name ?? 'New Role');
let description = $ref(role?.description ?? ''); let description = $ref(role?.description ?? '');
let rolePermission = $ref(role?.isAdministrator ? 'administrator' : role?.isModerator ? 'moderator' : 'normal'); let rolePermission = $ref(role?.isAdministrator ? 'administrator' : role?.isModerator ? 'moderator' : 'normal');
let color = $ref(role?.color ?? null); let color = $ref(role?.color ?? null);
let iconUrl = $ref(role?.iconUrl ?? null);
let target = $ref(role?.target ?? 'manual'); let target = $ref(role?.target ?? 'manual');
let condFormula = $ref(role?.condFormula ?? { id: uuid(), type: 'isRemote' }); let condFormula = $ref(role?.condFormula ?? { id: uuid(), type: 'isRemote' });
let isPublic = $ref(role?.isPublic ?? false); let isPublic = $ref(role?.isPublic ?? false);
let asBadge = $ref(role?.asBadge ?? false);
let canEditMembersByModerator = $ref(role?.canEditMembersByModerator ?? false); let canEditMembersByModerator = $ref(role?.canEditMembersByModerator ?? false);
const policies = reactive<Record<typeof ROLE_POLICIES[number], { useDefault: boolean; priority: number; value: any; }>>({}); const policies = reactive<Record<typeof ROLE_POLICIES[number], { useDefault: boolean; priority: number; value: any; }>>({});
@ -466,11 +477,13 @@ async function save() {
name, name,
description, description,
color: color === '' ? null : color, color: color === '' ? null : color,
iconUrl: iconUrl === '' ? null : iconUrl,
target, target,
condFormula, condFormula,
isAdministrator: rolePermission === 'administrator', isAdministrator: rolePermission === 'administrator',
isModerator: rolePermission === 'moderator', isModerator: rolePermission === 'moderator',
isPublic, isPublic,
asBadge,
canEditMembersByModerator, canEditMembersByModerator,
policies, policies,
}); });
@ -480,11 +493,13 @@ async function save() {
name, name,
description, description,
color: color === '' ? null : color, color: color === '' ? null : color,
iconUrl: iconUrl === '' ? null : iconUrl,
target, target,
condFormula, condFormula,
isAdministrator: rolePermission === 'administrator', isAdministrator: rolePermission === 'administrator',
isModerator: rolePermission === 'moderator', isModerator: rolePermission === 'moderator',
isPublic, isPublic,
asBadge,
canEditMembersByModerator, canEditMembersByModerator,
policies, policies,
}); });

View file

@ -155,7 +155,11 @@ async function run() {
os.inputText({ os.inputText({
title: q, title: q,
}).then(({ canceled, result: a }) => { }).then(({ canceled, result: a }) => {
ok(a); if (canceled) {
ok('');
} else {
ok(a);
}
}); });
}); });
}, },

View file

@ -86,7 +86,11 @@ async function run() {
os.inputText({ os.inputText({
title: q, title: q,
}).then(({ canceled, result: a }) => { }).then(({ canceled, result: a }) => {
ok(a); if (canceled) {
ok('');
} else {
ok(a);
}
}); });
}); });
}, },

View file

@ -31,6 +31,7 @@
<div class="_buttons"> <div class="_buttons">
<MkButton primary inline @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton> <MkButton primary inline @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton>
<MkButton danger inline @click="del"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
</div> </div>
</div> </div>
</template> </template>
@ -44,6 +45,9 @@ import MkButton from '@/components/MkButton.vue';
import * as os from '@/os'; import * as os from '@/os';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata'; import { definePageMetadata } from '@/scripts/page-metadata';
import { useRouter } from '@/router';
const router = useRouter();
const props = defineProps<{ const props = defineProps<{
webhookId: string; webhookId: string;
@ -86,6 +90,19 @@ async function save(): Promise<void> {
}); });
} }
async function del(): Promise<void> {
const { canceled } = await os.confirm({
type: 'warning',
text: i18n.t('deleteAreYouSure', { x: webhook.name }),
});
if (canceled) return;
await os.apiWithDialog('i/webhooks/delete', {
webhookId: props.webhookId,
});
router.push('/settings/webhook');
}
const headerActions = $computed(() => []); const headerActions = $computed(() => []);
const headerTabs = $computed(() => []); const headerTabs = $computed(() => []);

View file

@ -1,9 +1,9 @@
<template> <template>
<MkStickyContainer> <MkStickyContainer>
<template #header><MkPageHeader v-model:tab="src" :actions="headerActions" :tabs="headerTabs" :display-my-avatar="true"/></template> <template #header><MkPageHeader v-model:tab="src" :actions="headerActions" :tabs="$i ? headerTabs : headerTabsWhenNotLogin" :display-my-avatar="true"/></template>
<MkSpacer :content-max="800"> <MkSpacer :content-max="800">
<div ref="rootEl" v-hotkey.global="keymap"> <div ref="rootEl" v-hotkey.global="keymap">
<XTutorial v-if="$store.reactiveState.tutorial.value != -1" class="_panel" style="margin-bottom: var(--margin);"/> <XTutorial v-if="$i && $store.reactiveState.tutorial.value != -1" class="_panel" style="margin-bottom: var(--margin);"/>
<XPostForm v-if="$store.reactiveState.showFixedPostForm.value" :class="$style.postForm" class="post-form _panel" fixed style="margin-bottom: var(--margin);"/> <XPostForm v-if="$store.reactiveState.showFixedPostForm.value" :class="$style.postForm" class="post-form _panel" fixed style="margin-bottom: var(--margin);"/>
<div v-if="queue > 0" :class="$style.new"><button class="_buttonPrimary" @click="top()">{{ i18n.ts.newNoteRecived }}</button></div> <div v-if="queue > 0" :class="$style.new"><button class="_buttonPrimary" @click="top()">{{ i18n.ts.newNoteRecived }}</button></div>
@ -45,7 +45,8 @@ const tlComponent = $shallowRef<InstanceType<typeof XTimeline>>();
const rootEl = $shallowRef<HTMLElement>(); const rootEl = $shallowRef<HTMLElement>();
let queue = $ref(0); let queue = $ref(0);
const src = $computed({ get: () => defaultStore.reactiveState.tl.value.src, set: (x) => saveSrc(x) }); let srcWhenNotSignin = $ref(isLocalTimelineAvailable ? 'local' : 'global');
const src = $computed({ get: () => ($i ? defaultStore.reactiveState.tl.value.src : srcWhenNotSignin), set: (x) => saveSrc(x) });
watch ($$(src), () => queue = 0); watch ($$(src), () => queue = 0);
@ -94,6 +95,7 @@ function saveSrc(newSrc: 'home' | 'local' | 'social' | 'global'): void {
...defaultStore.state.tl, ...defaultStore.state.tl,
src: newSrc, src: newSrc,
}); });
srcWhenNotSignin = newSrc;
} }
async function timetravel(): Promise<void> { async function timetravel(): Promise<void> {
@ -148,6 +150,21 @@ const headerTabs = $computed(() => [{
onClick: chooseChannel, onClick: chooseChannel,
}]); }]);
const headerTabsWhenNotLogin = $computed(() => [
...(isLocalTimelineAvailable ? [{
key: 'local',
title: i18n.ts._timelines.local,
icon: 'ti ti-planet',
iconOnly: true,
}] : []),
...(isGlobalTimelineAvailable ? [{
key: 'global',
title: i18n.ts._timelines.global,
icon: 'ti ti-whirl',
iconOnly: true,
}] : []),
]);
definePageMetadata(computed(() => ({ definePageMetadata(computed(() => ({
title: i18n.ts.timeline, title: i18n.ts.timeline,
icon: src === 'local' ? 'ti ti-planet' : src === 'social' ? 'ti ti-rocket' : src === 'global' ? 'ti ti-whirl' : 'ti ti-home', icon: src === 'local' ? 'ti ti-planet' : src === 'social' ? 'ti ti-rocket' : src === 'global' ? 'ti ti-whirl' : 'ti ti-home',

View file

@ -39,7 +39,10 @@
</div> </div>
</div> </div>
<div v-if="user.roles.length > 0" class="roles"> <div v-if="user.roles.length > 0" class="roles">
<span v-for="role in user.roles" :key="role.id" v-tooltip="role.description" class="role" :style="{ '--color': role.color }">{{ role.name }}</span> <span v-for="role in user.roles" :key="role.id" v-tooltip="role.description" class="role" :style="{ '--color': role.color }">
<img v-if="role.iconUrl" style="height: 1.3em; vertical-align: -22%;" :src="role.iconUrl"/>
{{ role.name }}
</span>
</div> </div>
<div class="description"> <div class="description">
<MkOmit> <MkOmit>

View file

@ -20,7 +20,11 @@ export function install(plugin) {
inputText({ inputText({
title: q, title: q,
}).then(({ canceled, result: a }) => { }).then(({ canceled, result: a }) => {
ok(a); if (canceled) {
ok('');
} else {
ok(a);
}
}); });
}); });
}, },

View file

@ -484,6 +484,9 @@ export const routes = [{
path: '/clicker', path: '/clicker',
component: page(() => import('./pages/clicker.vue')), component: page(() => import('./pages/clicker.vue')),
loginRequired: true, loginRequired: true,
}, {
path: '/timeline',
component: page(() => import('./pages/timeline.vue')),
}, { }, {
name: 'index', name: 'index',
path: '/', path: '/',

View file

@ -27,7 +27,11 @@ export function createAiScriptEnv(opts) {
return confirm.canceled ? values.FALSE : values.TRUE; return confirm.canceled ? values.FALSE : values.TRUE;
}), }),
'Mk:api': values.FN_NATIVE(async ([ep, param, token]) => { 'Mk:api': values.FN_NATIVE(async ([ep, param, token]) => {
if (token) utils.assertString(token); if (token) {
utils.assertString(token);
// バグがあればundefinedもあり得るため念のため
if (typeof token.value !== 'string') throw new Error('invalid token');
}
apiRequests++; apiRequests++;
if (apiRequests > 16) return values.NULL; if (apiRequests > 16) return values.NULL;
const res = await os.api(ep.value, utils.valToJs(param), token ? token.value : (opts.token ?? null)); const res = await os.api(ep.value, utils.valToJs(param), token ? token.value : (opts.token ?? null));

View file

@ -5,14 +5,14 @@
<script lang="ts"> <script lang="ts">
import { defineComponent, defineAsyncComponent } from 'vue'; import { defineComponent, defineAsyncComponent } from 'vue';
import DesignA from './visitor/a.vue'; //import DesignA from './visitor/a.vue';
import DesignB from './visitor/b.vue'; import DesignB from './visitor/b.vue';
import XCommon from './_common_/common.vue'; import XCommon from './_common_/common.vue';
export default defineComponent({ export default defineComponent({
components: { components: {
XCommon, XCommon,
DesignA, //DesignA,
DesignB, DesignB,
}, },
}); });

View file

@ -10,7 +10,7 @@
<XKanban v-if="narrow && !root" class="banner" :powered-by="root"/> <XKanban v-if="narrow && !root" class="banner" :powered-by="root"/>
<div class="contents"> <div class="contents">
<XHeader v-if="!root" class="header" :info="pageInfo"/> <XHeader v-if="!root" class="header"/>
<main style="container-type: inline-size;"> <main style="container-type: inline-size;">
<RouterView/> <RouterView/>
</main> </main>
@ -33,9 +33,14 @@
<Transition :name="$store.state.animation ? 'tray' : ''"> <Transition :name="$store.state.animation ? 'tray' : ''">
<div v-if="showMenu" class="menu"> <div v-if="showMenu" class="menu">
<MkA to="/" class="link" active-class="active"><i class="ti ti-home icon"></i>{{ $ts.home }}</MkA> <MkA to="/" class="link" active-class="active"><i class="ti ti-home icon"></i>{{ $ts.home }}</MkA>
<MkA v-if="isTimelineAvailable" to="/timeline" class="link" active-class="active"><i class="ti ti-message icon"></i>{{ $ts.timeline }}</MkA>
<MkA to="/explore" class="link" active-class="active"><i class="ti ti-hash icon"></i>{{ $ts.explore }}</MkA> <MkA to="/explore" class="link" active-class="active"><i class="ti ti-hash icon"></i>{{ $ts.explore }}</MkA>
<MkA to="/featured" class="link" active-class="active"><i class="ti ti-flare icon"></i>{{ $ts.featured }}</MkA> <MkA to="/announcements" class="link" active-class="active"><i class="ti ti-speakerphone icon"></i>{{ $ts.announcements }}</MkA>
<MkA to="/channels" class="link" active-class="active"><i class="ti ti-device-tv icon"></i>{{ $ts.channel }}</MkA> <MkA to="/channels" class="link" active-class="active"><i class="ti ti-device-tv icon"></i>{{ $ts.channel }}</MkA>
<div class="divider"></div>
<MkA to="/pages" class="link" active-class="active"><i class="ti ti-news icon"></i>{{ $ts.pages }}</MkA>
<MkA to="/play" class="link" active-class="active"><i class="ti ti-player-play icon"></i>Play</MkA>
<MkA to="/gallery" class="link" active-class="active"><i class="ti ti-icons icon"></i>{{ $ts.gallery }}</MkA>
<div class="action"> <div class="action">
<button class="_buttonPrimary" @click="signup()">{{ $ts.signup }}</button> <button class="_buttonPrimary" @click="signup()">{{ $ts.signup }}</button>
<button class="_button" @click="signin()">{{ $ts.login }}</button> <button class="_button" @click="signin()">{{ $ts.login }}</button>
@ -52,6 +57,7 @@ import XKanban from './kanban.vue';
import { host, instanceName } from '@/config'; import { host, instanceName } from '@/config';
import { search } from '@/scripts/search'; import { search } from '@/scripts/search';
import * as os from '@/os'; import * as os from '@/os';
import { instance } from '@/instance';
import MkPagination from '@/components/MkPagination.vue'; import MkPagination from '@/components/MkPagination.vue';
import XSigninDialog from '@/components/MkSigninDialog.vue'; import XSigninDialog from '@/components/MkSigninDialog.vue';
import XSignupDialog from '@/components/MkSignupDialog.vue'; import XSignupDialog from '@/components/MkSignupDialog.vue';
@ -76,6 +82,9 @@ const announcements = {
endpoint: 'announcements', endpoint: 'announcements',
limit: 10, limit: 10,
}; };
const isTimelineAvailable = instance.policies.ltlAvailable || instance.policies.gtlAvailable;
let showMenu = $ref(false); let showMenu = $ref(false);
let isDesktop = $ref(window.innerWidth >= DESKTOP_THRESHOLD); let isDesktop = $ref(window.innerWidth >= DESKTOP_THRESHOLD);
let narrow = $ref(window.innerWidth < 1280); let narrow = $ref(window.innerWidth < 1280);
@ -223,6 +232,12 @@ defineExpose({
} }
} }
> .divider {
margin: 8px auto;
width: calc(100% - 32px);
border-top: solid 0.5px var(--divider);
}
> .action { > .action {
padding: 16px; padding: 16px;

View file

@ -3,18 +3,9 @@
<div v-if="narrow === false" class="wide"> <div v-if="narrow === false" class="wide">
<div class="content"> <div class="content">
<MkA to="/" class="link" active-class="active"><i class="ti ti-home icon"></i>{{ $ts.home }}</MkA> <MkA to="/" class="link" active-class="active"><i class="ti ti-home icon"></i>{{ $ts.home }}</MkA>
<MkA v-if="isTimelineAvailable" to="/timeline" class="link" active-class="active"><i class="ti ti-message icon"></i>{{ $ts.timeline }}</MkA>
<MkA to="/explore" class="link" active-class="active"><i class="ti ti-hash icon"></i>{{ $ts.explore }}</MkA> <MkA to="/explore" class="link" active-class="active"><i class="ti ti-hash icon"></i>{{ $ts.explore }}</MkA>
<MkA to="/featured" class="link" active-class="active"><i class="ti ti-flare icon"></i>{{ $ts.featured }}</MkA>
<MkA to="/channels" class="link" active-class="active"><i class="ti ti-device-tv icon"></i>{{ $ts.channel }}</MkA> <MkA to="/channels" class="link" active-class="active"><i class="ti ti-device-tv icon"></i>{{ $ts.channel }}</MkA>
<div v-if="info" class="page active link">
<div class="title">
<i v-if="info.icon" class="icon" :class="info.icon"></i>
<MkAvatar v-else-if="info.avatar" class="avatar" :user="info.avatar" indicator/>
<span v-if="info.title" class="text">{{ info.title }}</span>
<MkUserName v-else-if="info.userName" :user="info.userName" :nowrap="false" class="text"/>
</div>
<button v-if="info.action" class="_button action" @click.stop="info.action.handler"><!-- TODO --></button>
</div>
<div class="right"> <div class="right">
<button class="_button search" @click="search()"><i class="ti ti-search icon"></i><span>{{ $ts.search }}</span></button> <button class="_button search" @click="search()"><i class="ti ti-search icon"></i><span>{{ $ts.search }}</span></button>
<button class="_buttonPrimary signup" @click="signup()">{{ $ts.signup }}</button> <button class="_buttonPrimary signup" @click="signup()">{{ $ts.signup }}</button>
@ -26,15 +17,6 @@
<button class="menu _button" @click="$parent.showMenu = true"> <button class="menu _button" @click="$parent.showMenu = true">
<i class="ti ti-menu-2 icon"></i> <i class="ti ti-menu-2 icon"></i>
</button> </button>
<div v-if="info" class="title">
<i v-if="info.icon" class="icon" :class="info.icon"></i>
<MkAvatar v-else-if="info.avatar" class="avatar" :user="info.avatar" indicator/>
<span v-if="info.title" class="text">{{ info.title }}</span>
<MkUserName v-else-if="info.userName" :user="info.userName" :nowrap="false" class="text"/>
</div>
<button v-if="info && info.action" class="action _button" @click.stop="info.action.handler">
<!-- TODO -->
</button>
</div> </div>
</div> </div>
</template> </template>
@ -44,19 +26,15 @@ import { defineComponent } from 'vue';
import XSigninDialog from '@/components/MkSigninDialog.vue'; import XSigninDialog from '@/components/MkSigninDialog.vue';
import XSignupDialog from '@/components/MkSignupDialog.vue'; import XSignupDialog from '@/components/MkSignupDialog.vue';
import * as os from '@/os'; import * as os from '@/os';
import { instance } from '@/instance';
import { search } from '@/scripts/search'; import { search } from '@/scripts/search';
export default defineComponent({ export default defineComponent({
props: {
info: {
required: true,
},
},
data() { data() {
return { return {
narrow: null, narrow: null,
showMenu: false, showMenu: false,
isTimelineAvailable: instance.policies.ltlAvailable || instance.policies.gtlAvailable,
}; };
}, },
@ -84,8 +62,9 @@ export default defineComponent({
<style lang="scss" scoped> <style lang="scss" scoped>
.sqxihjet { .sqxihjet {
$height: 60px; $height: 50px;
position: sticky; position: sticky;
width: 50px;
top: 0; top: 0;
left: 0; left: 0;
z-index: 1000; z-index: 1000;

View file

@ -72,7 +72,11 @@ const run = async () => {
os.inputText({ os.inputText({
title: q, title: q,
}).then(({ canceled, result: a }) => { }).then(({ canceled, result: a }) => {
ok(a); if (canceled) {
ok('');
} else {
ok(a);
}
}); });
}); });
}, },

View file

@ -67,7 +67,11 @@ async function run() {
os.inputText({ os.inputText({
title: q, title: q,
}).then(({ canceled, result: a }) => { }).then(({ canceled, result: a }) => {
ok(a); if (canceled) {
ok('');
} else {
ok(a);
}
}); });
}); });
}, },

View file

@ -60,7 +60,11 @@ const run = async () => {
os.inputText({ os.inputText({
title: q, title: q,
}).then(({ canceled, result: a }) => { }).then(({ canceled, result: a }) => {
ok(a); if (canceled) {
ok('');
} else {
ok(a);
}
}); });
}); });
}, },