Merge branch 'develop'
This commit is contained in:
commit
baf65bfa69
|
@ -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
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
DKL-DI-0005
|
||||||
|
DKL-DI-0006
|
||||||
|
DKL-LI-0003
|
|
@ -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
|
||||||
|
|
|
@ -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}"
|
17
CHANGELOG.md
17
CHANGELOG.md
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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`.
|
||||||
|
|
||||||
|
|
11
Dockerfile
11
Dockerfile
|
@ -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"]
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
PORT=$(yq '.port' /misskey/.config/default.yml)
|
||||||
|
curl -s -S -o /dev/null "http://localhost:${PORT}"
|
|
@ -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"
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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: "優先度"
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
---
|
||||||
|
_lang_: "ພາສາລາວ"
|
|
@ -1195,6 +1195,9 @@ _role:
|
||||||
baseRole: "บทบาทพื้นฐาน"
|
baseRole: "บทบาทพื้นฐาน"
|
||||||
useBaseValue: "ใช้บทบาทพื้นฐานเริ่มต้น"
|
useBaseValue: "ใช้บทบาทพื้นฐานเริ่มต้น"
|
||||||
chooseRoleToAssign: "เลือกบทบาทที่ต้องการกำหนด"
|
chooseRoleToAssign: "เลือกบทบาทที่ต้องการกำหนด"
|
||||||
|
iconUrl: "ไอคอน URL"
|
||||||
|
asBadge: "แสดงเป็นตรา"
|
||||||
|
descriptionOfAsBadge: "ไอคอนของบทบาทนี้จะปรากฏถัดจากชื่อผู้ใช้ของผู้ใช้งานด้วยบทบาทนี้ถ้าหากเปิดใช้งาน"
|
||||||
canEditMembersByModerator: "อนุญาตให้ผู้ดูแลแก้ไขสมาชิก"
|
canEditMembersByModerator: "อนุญาตให้ผู้ดูแลแก้ไขสมาชิก"
|
||||||
descriptionOfCanEditMembersByModerator: "เมื่อเปิดใช้ ผู้ดูแลนอกเหนือจากผู้ดูแลระบบแล้ว จะสามารถกำหนดและยกเลิกการมอบหมายบทบาทนี้ให้กับผู้ใช้ได้ เมื่อปิด เฉพาะผู้ดูแลระบบเท่านั้นที่จะสามารถกำหนดผู้ใช้ได้นะ"
|
descriptionOfCanEditMembersByModerator: "เมื่อเปิดใช้ ผู้ดูแลนอกเหนือจากผู้ดูแลระบบแล้ว จะสามารถกำหนดและยกเลิกการมอบหมายบทบาทนี้ให้กับผู้ใช้ได้ เมื่อปิด เฉพาะผู้ดูแลระบบเท่านั้นที่จะสามารถกำหนดผู้ใช้ได้นะ"
|
||||||
priority: "ลำดับความสำคัญ"
|
priority: "ลำดับความสำคัญ"
|
||||||
|
|
|
@ -1195,6 +1195,9 @@ _role:
|
||||||
baseRole: "基本角色"
|
baseRole: "基本角色"
|
||||||
useBaseValue: "使用基本角色的值"
|
useBaseValue: "使用基本角色的值"
|
||||||
chooseRoleToAssign: "选择要分配的角色"
|
chooseRoleToAssign: "选择要分配的角色"
|
||||||
|
iconUrl: "图标URL"
|
||||||
|
asBadge: "作为徽章显示"
|
||||||
|
descriptionOfAsBadge: "开启后,用户名旁边将会出现角色图标。"
|
||||||
canEditMembersByModerator: "允许监察者编辑成员"
|
canEditMembersByModerator: "允许监察者编辑成员"
|
||||||
descriptionOfCanEditMembersByModerator: "如果选中,监察者和管理员都能够为用户分配/取消分配角色。如果未选中,则只有管理员可以执行此操作。"
|
descriptionOfCanEditMembersByModerator: "如果选中,监察者和管理员都能够为用户分配/取消分配角色。如果未选中,则只有管理员可以执行此操作。"
|
||||||
priority: "优先级"
|
priority: "优先级"
|
||||||
|
|
|
@ -1195,6 +1195,9 @@ _role:
|
||||||
baseRole: "基本角色"
|
baseRole: "基本角色"
|
||||||
useBaseValue: "使用基本角色的值"
|
useBaseValue: "使用基本角色的值"
|
||||||
chooseRoleToAssign: "選擇要指派的角色"
|
chooseRoleToAssign: "選擇要指派的角色"
|
||||||
|
iconUrl: "圖示的URL"
|
||||||
|
asBadge: "顯示為徽章"
|
||||||
|
descriptionOfAsBadge: "開啟的話,角色圖示會顯示在用戶名旁邊。"
|
||||||
canEditMembersByModerator: "允許編輯監察員的成員"
|
canEditMembersByModerator: "允許編輯監察員的成員"
|
||||||
descriptionOfCanEditMembersByModerator: "如果開啟,管理員與監察員都可以為使用者指派/解除指派該角色。如果關閉,則只有管理員可以執行。"
|
descriptionOfCanEditMembersByModerator: "如果開啟,管理員與監察員都可以為使用者指派/解除指派該角色。如果關閉,則只有管理員可以執行。"
|
||||||
priority: "優先級"
|
priority: "優先級"
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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"`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)) {
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
})
|
})
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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({
|
||||||
|
|
|
@ -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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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]));
|
||||||
|
|
|
@ -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,
|
||||||
});
|
});
|
||||||
|
|
|
@ -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');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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,
|
||||||
});
|
});
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
|
@ -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(() => []);
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
|
@ -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: '/',
|
||||||
|
|
|
@ -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));
|
||||||
|
|
|
@ -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,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in New Issue