Merge branch 'develop' into vue3
This commit is contained in:
commit
fc09479d0a
|
@ -12,6 +12,7 @@ RUN apk add --no-cache \
|
|||
autoconf \
|
||||
automake \
|
||||
file \
|
||||
git \
|
||||
g++ \
|
||||
gcc \
|
||||
libc-dev \
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
[![CircleCI](https://img.shields.io/circleci/project/github/syuilo/misskey.svg?style=for-the-badge&logo=circleci)](https://circleci.com/gh/syuilo/misskey)
|
||||
[![Dependencies](https://img.shields.io/david/syuilo/misskey.svg?style=for-the-badge&logo=npm)](https://david-dm.org/syuilo/misskey)
|
||||
[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=for-the-badge&logo=github)](http://makeapullrequest.com)
|
||||
[![Awesome Humane Tech](https://raw.githubusercontent.com/humanetech-community/awesome-humane-tech/main/humane-tech-badge.svg?sanitize=true)](https://github.com/humanetech-community/awesome-humane-tech)
|
||||
|
||||
**A forever evolving, sophisticated microblogging platform.**
|
||||
|
||||
|
|
|
@ -264,6 +264,7 @@ rename: "名前を変更"
|
|||
avatar: "アイコン"
|
||||
banner: "バナー"
|
||||
nsfw: "閲覧注意"
|
||||
whenServerDisconnected: "サーバーとの接続が失われたとき"
|
||||
disconnectedFromServer: "サーバーから切断されました"
|
||||
reload: "リロード"
|
||||
doNothing: "なにもしない"
|
||||
|
@ -364,7 +365,6 @@ unregister: "登録を解除"
|
|||
passwordLessLogin: "パスワード無しログイン"
|
||||
resetPassword: "パスワードをリセット"
|
||||
newPasswordIs: "新しいパスワードは「{password}」です"
|
||||
autoReloadWhenDisconnected: "サーバー切断時に自動リロード"
|
||||
autoNoteWatch: "ノートの自動ウォッチ"
|
||||
autoNoteWatchDescription: "あなたがリアクションしたり返信したりした他のユーザーのノートに関する通知を受け取るようにします。"
|
||||
reduceUiAnimation: "UIのアニメーションを減らす"
|
||||
|
@ -468,6 +468,7 @@ objectStorageUseSSL: "SSLを使用する"
|
|||
objectStorageUseSSLDesc: "API接続にhttpsを使用しない場合はオフにしてください"
|
||||
objectStorageUseProxy: "Proxyを利用する"
|
||||
objectStorageUseProxyDesc: "API接続にproxyを利用しない場合はオフにしてください"
|
||||
objectStorageSetPublicRead: "アップロード時に'public-read'を設定する"
|
||||
serverLogs: "サーバーログ"
|
||||
deleteAll: "全て削除"
|
||||
showFixedPostForm: "タイムライン上部に投稿フォームを表示する"
|
||||
|
@ -558,6 +559,29 @@ userSaysSomething: "{name}が何かを言いました"
|
|||
makeActive: "アクティブにする"
|
||||
display: "表示"
|
||||
copy: "コピー"
|
||||
metrics: "メトリクス"
|
||||
overview: "概要"
|
||||
logs: "ログ"
|
||||
delayed: "遅延"
|
||||
database: "データベース"
|
||||
channel: "チャンネル"
|
||||
create: "作成"
|
||||
|
||||
_serverDisconnectedBehavior:
|
||||
reload: "自動でリロード"
|
||||
dialog: "ダイアログで警告"
|
||||
quiet: "控えめに警告"
|
||||
|
||||
_channel:
|
||||
create: "チャンネルを作成"
|
||||
edit: "チャンネルを編集"
|
||||
setBanner: "バナーを設定"
|
||||
removeBanner: "バナーを削除"
|
||||
featured: "トレンド"
|
||||
owned: "管理中"
|
||||
following: "フォロー中"
|
||||
usersCount: "{n}人が参加中"
|
||||
notesCount: "{n}投稿があります"
|
||||
|
||||
_sidebar:
|
||||
full: "フル"
|
||||
|
@ -572,6 +596,7 @@ _wordMute:
|
|||
hardDescription: "指定した条件のノートをタイムラインに追加しないようにします。追加されなかったノートは、条件を変更しても除外されたままになります。"
|
||||
soft: "ソフト"
|
||||
hard: "ハード"
|
||||
mutedNotes: "ミュートされたノート"
|
||||
|
||||
_theme:
|
||||
explore: "テーマを探す"
|
||||
|
@ -653,6 +678,7 @@ _sfx:
|
|||
chat: "チャット"
|
||||
chatBg: "チャット(バックグラウンド)"
|
||||
antenna: "アンテナ受信"
|
||||
channel: "チャンネル通知"
|
||||
|
||||
_ago:
|
||||
unknown: "謎"
|
||||
|
@ -733,6 +759,8 @@ _permissions:
|
|||
"write:page-likes": "ページのいいねを操作する"
|
||||
"read:user-groups": "ユーザーグループを見る"
|
||||
"write:user-groups": "ユーザーグループを操作する"
|
||||
"read:channels": "チャンネルを見る"
|
||||
"write:channels": "チャンネルを操作する"
|
||||
|
||||
_auth:
|
||||
shareAccess: "「{name}」がアカウントにアクセスすることを許可しますか?"
|
||||
|
@ -815,6 +843,7 @@ _visibility:
|
|||
_postForm:
|
||||
replyPlaceholder: "このノートに返信..."
|
||||
quotePlaceholder: "このノートを引用..."
|
||||
channelPlaceholder: "チャンネルに投稿..."
|
||||
_placeholders:
|
||||
a: "いまどうしてる?"
|
||||
b: "何かありましたか?"
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
import {MigrationInterface, QueryRunner} from "typeorm";
|
||||
|
||||
export class channel1596548170836 implements MigrationInterface {
|
||||
name = 'channel1596548170836'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`CREATE TABLE "channel" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "lastNotedAt" TIMESTAMP WITH TIME ZONE, "userId" character varying(32) NOT NULL, "name" character varying(128) NOT NULL, "description" character varying(2048), "bannerId" character varying(32), "notesCount" integer NOT NULL DEFAULT 0, "usersCount" integer NOT NULL DEFAULT 0, CONSTRAINT "PK_590f33ee6ee7d76437acf362e39" PRIMARY KEY ("id"))`);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_71cb7b435b7c0d4843317e7e16" ON "channel" ("createdAt") `);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_29ef80c6f13bcea998447fce43" ON "channel" ("lastNotedAt") `);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_823bae55bd81b3be6e05cff438" ON "channel" ("userId") `);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_0f58c11241e649d2a638a8de94" ON "channel" ("notesCount") `);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_094b86cd36bb805d1aa1e8cc9a" ON "channel" ("usersCount") `);
|
||||
await queryRunner.query(`CREATE TABLE "channel_following" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "followeeId" character varying(32) NOT NULL, "followerId" character varying(32) NOT NULL, CONSTRAINT "PK_8b104be7f7415113f2a02cd5bdd" PRIMARY KEY ("id"))`);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_11e71f2511589dcc8a4d3214f9" ON "channel_following" ("createdAt") `);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_0e43068c3f92cab197c3d3cd86" ON "channel_following" ("followeeId") `);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_6d8084ec9496e7334a4602707e" ON "channel_following" ("followerId") `);
|
||||
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_2e230dd45a10e671d781d99f3e" ON "channel_following" ("followerId", "followeeId") `);
|
||||
await queryRunner.query(`CREATE TABLE "channel_note_pining" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "channelId" character varying(32) NOT NULL, "noteId" character varying(32) NOT NULL, CONSTRAINT "PK_44f7474496bcf2e4b741681146d" PRIMARY KEY ("id"))`);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_8125f950afd3093acb10d2db8a" ON "channel_note_pining" ("channelId") `);
|
||||
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_f36fed37d6d4cdcc68c803cd9c" ON "channel_note_pining" ("channelId", "noteId") `);
|
||||
await queryRunner.query(`ALTER TABLE "note" ADD "channelId" character varying(32) DEFAULT null`);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_f22169eb10657bded6d875ac8f" ON "note" ("channelId") `);
|
||||
await queryRunner.query(`ALTER TABLE "channel" ADD CONSTRAINT "FK_823bae55bd81b3be6e05cff4383" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE SET NULL ON UPDATE NO ACTION`);
|
||||
await queryRunner.query(`ALTER TABLE "channel" ADD CONSTRAINT "FK_999da2bcc7efadbfe0e92d3bc19" FOREIGN KEY ("bannerId") REFERENCES "drive_file"("id") ON DELETE SET NULL ON UPDATE NO ACTION`);
|
||||
await queryRunner.query(`ALTER TABLE "note" ADD CONSTRAINT "FK_f22169eb10657bded6d875ac8f9" FOREIGN KEY ("channelId") REFERENCES "channel"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||
await queryRunner.query(`ALTER TABLE "channel_following" ADD CONSTRAINT "FK_0e43068c3f92cab197c3d3cd86e" FOREIGN KEY ("followeeId") REFERENCES "channel"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||
await queryRunner.query(`ALTER TABLE "channel_following" ADD CONSTRAINT "FK_6d8084ec9496e7334a4602707e1" FOREIGN KEY ("followerId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||
await queryRunner.query(`ALTER TABLE "channel_note_pining" ADD CONSTRAINT "FK_8125f950afd3093acb10d2db8a8" FOREIGN KEY ("channelId") REFERENCES "channel"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||
await queryRunner.query(`ALTER TABLE "channel_note_pining" ADD CONSTRAINT "FK_10b19ef67d297ea9de325cd4502" FOREIGN KEY ("noteId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "channel_note_pining" DROP CONSTRAINT "FK_10b19ef67d297ea9de325cd4502"`);
|
||||
await queryRunner.query(`ALTER TABLE "channel_note_pining" DROP CONSTRAINT "FK_8125f950afd3093acb10d2db8a8"`);
|
||||
await queryRunner.query(`ALTER TABLE "channel_following" DROP CONSTRAINT "FK_6d8084ec9496e7334a4602707e1"`);
|
||||
await queryRunner.query(`ALTER TABLE "channel_following" DROP CONSTRAINT "FK_0e43068c3f92cab197c3d3cd86e"`);
|
||||
await queryRunner.query(`ALTER TABLE "note" DROP CONSTRAINT "FK_f22169eb10657bded6d875ac8f9"`);
|
||||
await queryRunner.query(`ALTER TABLE "channel" DROP CONSTRAINT "FK_999da2bcc7efadbfe0e92d3bc19"`);
|
||||
await queryRunner.query(`ALTER TABLE "channel" DROP CONSTRAINT "FK_823bae55bd81b3be6e05cff4383"`);
|
||||
await queryRunner.query(`DROP INDEX "IDX_f22169eb10657bded6d875ac8f"`);
|
||||
await queryRunner.query(`ALTER TABLE "note" DROP COLUMN "channelId"`);
|
||||
await queryRunner.query(`DROP INDEX "IDX_f36fed37d6d4cdcc68c803cd9c"`);
|
||||
await queryRunner.query(`DROP INDEX "IDX_8125f950afd3093acb10d2db8a"`);
|
||||
await queryRunner.query(`DROP TABLE "channel_note_pining"`);
|
||||
await queryRunner.query(`DROP INDEX "IDX_2e230dd45a10e671d781d99f3e"`);
|
||||
await queryRunner.query(`DROP INDEX "IDX_6d8084ec9496e7334a4602707e"`);
|
||||
await queryRunner.query(`DROP INDEX "IDX_0e43068c3f92cab197c3d3cd86"`);
|
||||
await queryRunner.query(`DROP INDEX "IDX_11e71f2511589dcc8a4d3214f9"`);
|
||||
await queryRunner.query(`DROP TABLE "channel_following"`);
|
||||
await queryRunner.query(`DROP INDEX "IDX_094b86cd36bb805d1aa1e8cc9a"`);
|
||||
await queryRunner.query(`DROP INDEX "IDX_0f58c11241e649d2a638a8de94"`);
|
||||
await queryRunner.query(`DROP INDEX "IDX_823bae55bd81b3be6e05cff438"`);
|
||||
await queryRunner.query(`DROP INDEX "IDX_29ef80c6f13bcea998447fce43"`);
|
||||
await queryRunner.query(`DROP INDEX "IDX_71cb7b435b7c0d4843317e7e16"`);
|
||||
await queryRunner.query(`DROP TABLE "channel"`);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
import {MigrationInterface, QueryRunner} from "typeorm";
|
||||
|
||||
export class channel21596786425167 implements MigrationInterface {
|
||||
name = 'channel21596786425167'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "channel_following" ADD "readCursor" TIMESTAMP WITH TIME ZONE NOT NULL`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "channel_following" DROP COLUMN "readCursor"`);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
import {MigrationInterface, QueryRunner} from "typeorm";
|
||||
|
||||
export class objectStorageSetPublicRead1597230137744 implements MigrationInterface {
|
||||
name = 'objectStorageSetPublicRead1597230137744'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "meta" ADD "objectStorageSetPublicRead" boolean NOT NULL DEFAULT false`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "objectStorageSetPublicRead"`);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
import {MigrationInterface, QueryRunner} from "typeorm";
|
||||
|
||||
export class addSensitiveIndex1597385880794 implements MigrationInterface {
|
||||
name = 'addSensitiveIndex1597385880794'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`CREATE INDEX "IDX_a7eba67f8b3fa27271e85d2e26" ON "drive_file" ("isSensitive") `);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`DROP INDEX "IDX_a7eba67f8b3fa27271e85d2e26"`);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
import {MigrationInterface, QueryRunner} from "typeorm";
|
||||
|
||||
export class channelUnread1597459042300 implements MigrationInterface {
|
||||
name = 'channelUnread1597459042300'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`TRUNCATE TABLE "note_unread"`, undefined);
|
||||
await queryRunner.query(`ALTER TABLE "channel_following" DROP COLUMN "readCursor"`);
|
||||
await queryRunner.query(`ALTER TABLE "note_unread" ADD "isMentioned" boolean NOT NULL`);
|
||||
await queryRunner.query(`ALTER TABLE "note_unread" ADD "noteChannelId" character varying(32)`);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_25b1dd384bec391b07b74b861c" ON "note_unread" ("isMentioned") `);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_89a29c9237b8c3b6b3cbb4cb30" ON "note_unread" ("isSpecified") `);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_29e8c1d579af54d4232939f994" ON "note_unread" ("noteUserId") `);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_6a57f051d82c6d4036c141e107" ON "note_unread" ("noteChannelId") `);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`DROP INDEX "IDX_6a57f051d82c6d4036c141e107"`);
|
||||
await queryRunner.query(`DROP INDEX "IDX_29e8c1d579af54d4232939f994"`);
|
||||
await queryRunner.query(`DROP INDEX "IDX_89a29c9237b8c3b6b3cbb4cb30"`);
|
||||
await queryRunner.query(`DROP INDEX "IDX_25b1dd384bec391b07b74b861c"`);
|
||||
await queryRunner.query(`ALTER TABLE "note_unread" DROP COLUMN "noteChannelId"`);
|
||||
await queryRunner.query(`ALTER TABLE "note_unread" DROP COLUMN "isMentioned"`);
|
||||
await queryRunner.query(`ALTER TABLE "channel_following" ADD "readCursor" TIMESTAMP WITH TIME ZONE NOT NULL`);
|
||||
}
|
||||
|
||||
}
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,141 @@
|
|||
<template>
|
||||
<button class="hdcaacmi _button"
|
||||
:class="{ wait, active: isFollowing, full }"
|
||||
@click="onClick"
|
||||
:disabled="wait"
|
||||
>
|
||||
<template v-if="!wait">
|
||||
<template v-if="isFollowing">
|
||||
<span v-if="full">{{ $t('unfollow') }}</span><fa :icon="faMinus"/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span v-if="full">{{ $t('follow') }}</span><fa :icon="faPlus"/>
|
||||
</template>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span v-if="full">{{ $t('processing') }}</span><fa :icon="faSpinner" pulse fixed-width/>
|
||||
</template>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { faSpinner, faPlus, faMinus, } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
export default Vue.extend({
|
||||
props: {
|
||||
channel: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
full: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
isFollowing: this.channel.isFollowing,
|
||||
wait: false,
|
||||
faSpinner, faPlus, faMinus,
|
||||
};
|
||||
},
|
||||
|
||||
methods: {
|
||||
async onClick() {
|
||||
this.wait = true;
|
||||
|
||||
try {
|
||||
if (this.isFollowing) {
|
||||
await this.$root.api('channels/unfollow', {
|
||||
channelId: this.channel.id
|
||||
});
|
||||
this.isFollowing = false;
|
||||
} else {
|
||||
await this.$root.api('channels/follow', {
|
||||
channelId: this.channel.id
|
||||
});
|
||||
this.isFollowing = true;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
this.wait = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.hdcaacmi {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
font-weight: bold;
|
||||
color: var(--accent);
|
||||
background: transparent;
|
||||
border: solid 1px var(--accent);
|
||||
padding: 0;
|
||||
height: 31px;
|
||||
font-size: 16px;
|
||||
border-radius: 32px;
|
||||
background: #fff;
|
||||
|
||||
&.full {
|
||||
padding: 0 8px 0 12px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
&:not(.full) {
|
||||
width: 31px;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
&:after {
|
||||
content: "";
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
top: -5px;
|
||||
right: -5px;
|
||||
bottom: -5px;
|
||||
left: -5px;
|
||||
border: 2px solid var(--focus);
|
||||
border-radius: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
//background: mix($primary, #fff, 20);
|
||||
}
|
||||
|
||||
&:active {
|
||||
//background: mix($primary, #fff, 40);
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: #fff;
|
||||
background: var(--accent);
|
||||
|
||||
&:hover {
|
||||
background: var(--accentLighten);
|
||||
border-color: var(--accentLighten);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: var(--accentDarken);
|
||||
border-color: var(--accentDarken);
|
||||
}
|
||||
}
|
||||
|
||||
&.wait {
|
||||
cursor: wait !important;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
> span {
|
||||
margin-right: 6px;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,144 @@
|
|||
<template>
|
||||
<router-link :to="`/channels/${channel.id}`" class="eftoefju _panel" tabindex="-1">
|
||||
<div class="banner" v-if="channel.bannerUrl" :style="`background-image: url('${channel.bannerUrl}')`">
|
||||
<div class="fade"></div>
|
||||
<div class="name"><fa :icon="faSatelliteDish"/> {{ channel.name }}</div>
|
||||
<div class="status">
|
||||
<div><fa :icon="faUsers" fixed-width/><i18n path="_channel.usersCount" tag="span" style="margin-left: 4px;"><b place="n">{{ channel.usersCount }}</b></i18n></div>
|
||||
<div><fa :icon="faPencilAlt" fixed-width/><i18n path="_channel.notesCount" tag="span" style="margin-left: 4px;"><b place="n">{{ channel.notesCount }}</b></i18n></div>
|
||||
</div>
|
||||
</div>
|
||||
<article v-if="channel.description">
|
||||
<p :title="channel.description">{{ channel.description.length > 85 ? channel.description.slice(0, 85) + '…' : channel.description }}</p>
|
||||
</article>
|
||||
<footer>
|
||||
<span>
|
||||
{{ $t('updatedAt') }}: <mk-time :time="channel.lastNotedAt"/>
|
||||
</span>
|
||||
</footer>
|
||||
</router-link>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { faSatelliteDish, faUsers, faPencilAlt } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
export default Vue.extend({
|
||||
props: {
|
||||
channel: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
faSatelliteDish, faUsers, faPencilAlt,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.eftoefju {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
border: 1px solid var(--divider);
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
> .banner {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
|
||||
> .fade {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 64px;
|
||||
background: linear-gradient(0deg, var(--panel), var(--X15));
|
||||
}
|
||||
|
||||
> .name {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
left: 16px;
|
||||
padding: 12px 16px;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
color: #fff;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
> .status {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
bottom: 16px;
|
||||
right: 16px;
|
||||
padding: 8px 12px;
|
||||
font-size: 80%;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
border-radius: 6px;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
> article {
|
||||
padding: 16px;
|
||||
|
||||
> p {
|
||||
margin: 0;
|
||||
font-size: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
> footer {
|
||||
padding: 12px 16px;
|
||||
border-top: solid 1px var(--divider);
|
||||
|
||||
> span {
|
||||
opacity: 0.7;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 550px) {
|
||||
font-size: 0.9em;
|
||||
|
||||
> .banner {
|
||||
height: 80px;
|
||||
|
||||
> .status {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
> article {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
> footer {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 500px) {
|
||||
font-size: 0.8em;
|
||||
|
||||
> .banner {
|
||||
height: 70px;
|
||||
}
|
||||
|
||||
> article {
|
||||
padding: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div class="zbcjwnqg">
|
||||
<div class="zbcjwnqg" v-size="{ max: [550, 1200] }">
|
||||
<div class="stats" v-if="info">
|
||||
<div class="_panel">
|
||||
<div>
|
||||
|
@ -84,7 +84,7 @@
|
|||
</div>
|
||||
|
||||
<section class="_card">
|
||||
<div class="_title"><fa :icon="faChartBar"/> {{ $t('statistics') }}</div>
|
||||
<div class="_title" style="position: relative;"><fa :icon="faChartBar"/> {{ $t('statistics') }}<button @click="fetchChart" class="_button" style="position: absolute; right: 0; bottom: 0; top: 0; padding: inherit;"><fa :icon="faSync"/></button></div>
|
||||
<div class="_content" style="margin-top: -8px;">
|
||||
<div class="selects" style="display: flex;">
|
||||
<mk-select v-model="chartSrc" style="margin: 0; flex: 1;">
|
||||
|
@ -123,12 +123,11 @@
|
|||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import { faChartBar, faUser, faPencilAlt } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faChartBar, faUser, faPencilAlt, faSync } from '@fortawesome/free-solid-svg-icons';
|
||||
import Chart from 'chart.js';
|
||||
import MkSelect from './ui/select.vue';
|
||||
import number from '../filters/number';
|
||||
|
||||
const chartLimit = 90;
|
||||
const sum = (...arr) => arr.reduce((r, a) => r.map((b, i) => a[i] + b));
|
||||
const negate = arr => arr.map(x => -x);
|
||||
const alpha = (hex, a) => {
|
||||
|
@ -144,6 +143,19 @@ export default defineComponent({
|
|||
MkSelect
|
||||
},
|
||||
|
||||
props: {
|
||||
chartLimit: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 90
|
||||
},
|
||||
detailed: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
info: null,
|
||||
|
@ -160,7 +172,7 @@ export default defineComponent({
|
|||
chartInstance: null,
|
||||
chartSrc: 'notes',
|
||||
chartSpan: 'hour',
|
||||
faChartBar, faUser, faPencilAlt
|
||||
faChartBar, faUser, faPencilAlt, faSync
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -209,18 +221,23 @@ export default defineComponent({
|
|||
|
||||
this.now = new Date();
|
||||
|
||||
this.fetchChart();
|
||||
},
|
||||
|
||||
methods: {
|
||||
async fetchChart() {
|
||||
const [perHour, perDay] = await Promise.all([Promise.all([
|
||||
this.$root.api('charts/federation', { limit: chartLimit, span: 'hour' }),
|
||||
this.$root.api('charts/users', { limit: chartLimit, span: 'hour' }),
|
||||
this.$root.api('charts/active-users', { limit: chartLimit, span: 'hour' }),
|
||||
this.$root.api('charts/notes', { limit: chartLimit, span: 'hour' }),
|
||||
this.$root.api('charts/drive', { limit: chartLimit, span: 'hour' }),
|
||||
this.$root.api('charts/federation', { limit: this.chartLimit, span: 'hour' }),
|
||||
this.$root.api('charts/users', { limit: this.chartLimit, span: 'hour' }),
|
||||
this.$root.api('charts/active-users', { limit: this.chartLimit, span: 'hour' }),
|
||||
this.$root.api('charts/notes', { limit: this.chartLimit, span: 'hour' }),
|
||||
this.$root.api('charts/drive', { limit: this.chartLimit, span: 'hour' }),
|
||||
]), Promise.all([
|
||||
this.$root.api('charts/federation', { limit: chartLimit, span: 'day' }),
|
||||
this.$root.api('charts/users', { limit: chartLimit, span: 'day' }),
|
||||
this.$root.api('charts/active-users', { limit: chartLimit, span: 'day' }),
|
||||
this.$root.api('charts/notes', { limit: chartLimit, span: 'day' }),
|
||||
this.$root.api('charts/drive', { limit: chartLimit, span: 'day' }),
|
||||
this.$root.api('charts/federation', { limit: this.chartLimit, span: 'day' }),
|
||||
this.$root.api('charts/users', { limit: this.chartLimit, span: 'day' }),
|
||||
this.$root.api('charts/active-users', { limit: this.chartLimit, span: 'day' }),
|
||||
this.$root.api('charts/notes', { limit: this.chartLimit, span: 'day' }),
|
||||
this.$root.api('charts/drive', { limit: this.chartLimit, span: 'day' }),
|
||||
])]);
|
||||
|
||||
const chart = {
|
||||
|
@ -254,17 +271,19 @@ export default defineComponent({
|
|||
this.renderChart();
|
||||
},
|
||||
|
||||
methods: {
|
||||
renderChart() {
|
||||
if (this.chartInstance) {
|
||||
this.chartInstance.destroy();
|
||||
}
|
||||
|
||||
// TODO: var(--panel)の色が暗いか明るいかで判定する
|
||||
const gridColor = this.$store.state.device.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
|
||||
|
||||
Chart.defaults.global.defaultFontColor = getComputedStyle(document.documentElement).getPropertyValue('--fg');
|
||||
this.chartInstance = new Chart(this.$refs.chart, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: new Array(chartLimit).fill(0).map((_, i) => this.getDate(i).toLocaleString()).slice().reverse(),
|
||||
labels: new Array(this.chartLimit).fill(0).map((_, i) => this.getDate(i).toLocaleString()).slice().reverse(),
|
||||
datasets: this.data.series.map(x => ({
|
||||
label: x.name,
|
||||
data: x.data.slice().reverse(),
|
||||
|
@ -272,7 +291,9 @@ export default defineComponent({
|
|||
lineTension: 0,
|
||||
borderWidth: 2,
|
||||
borderColor: x.color,
|
||||
borderDash: x.borderDash || [],
|
||||
backgroundColor: alpha(x.color, 0.1),
|
||||
fill: x.fill == null ? true : x.fill,
|
||||
hidden: !!x.hidden
|
||||
}))
|
||||
},
|
||||
|
@ -294,17 +315,28 @@ export default defineComponent({
|
|||
},
|
||||
scales: {
|
||||
xAxes: [{
|
||||
type: 'time',
|
||||
time: {
|
||||
stepSize: 1,
|
||||
unit: this.chartSpan == 'day' ? 'month' : 'day',
|
||||
},
|
||||
gridLines: {
|
||||
display: false
|
||||
display: this.detailed,
|
||||
color: gridColor,
|
||||
zeroLineColor: gridColor,
|
||||
},
|
||||
ticks: {
|
||||
display: false
|
||||
display: this.detailed
|
||||
}
|
||||
}],
|
||||
yAxes: [{
|
||||
position: 'right',
|
||||
position: 'left',
|
||||
gridLines: {
|
||||
color: gridColor,
|
||||
zeroLineColor: gridColor,
|
||||
},
|
||||
ticks: {
|
||||
display: false
|
||||
display: this.detailed
|
||||
}
|
||||
}]
|
||||
},
|
||||
|
@ -326,7 +358,11 @@ export default defineComponent({
|
|||
},
|
||||
|
||||
format(arr) {
|
||||
return arr;
|
||||
const now = Date.now();
|
||||
return arr.map((v, i) => ({
|
||||
x: new Date(now - ((this.chartSpan == 'day' ? 86400000 :3600000 ) * i)),
|
||||
y: v
|
||||
}));
|
||||
},
|
||||
|
||||
federationInstancesChart(total: boolean): any {
|
||||
|
@ -348,6 +384,8 @@ export default defineComponent({
|
|||
name: 'All',
|
||||
type: 'line',
|
||||
color: '#008FFB',
|
||||
borderDash: [5, 5],
|
||||
fill: false,
|
||||
data: this.format(type == 'combined'
|
||||
? sum(this.stats.notes.local.inc, negate(this.stats.notes.local.dec), this.stats.notes.remote.inc, negate(this.stats.notes.remote.dec))
|
||||
: sum(this.stats.notes[type].inc, negate(this.stats.notes[type].dec))
|
||||
|
@ -464,7 +502,9 @@ export default defineComponent({
|
|||
series: [{
|
||||
name: 'All',
|
||||
type: 'line',
|
||||
color: '#008FFB',
|
||||
color: '#09d8e2',
|
||||
borderDash: [5, 5],
|
||||
fill: false,
|
||||
data: this.format(
|
||||
sum(
|
||||
this.stats.drive.local.incSize,
|
||||
|
@ -481,17 +521,17 @@ export default defineComponent({
|
|||
}, {
|
||||
name: 'Local -',
|
||||
type: 'area',
|
||||
color: '#008FFB',
|
||||
color: '#FF4560',
|
||||
data: this.format(negate(this.stats.drive.local.decSize))
|
||||
}, {
|
||||
name: 'Remote +',
|
||||
type: 'area',
|
||||
color: '#008FFB',
|
||||
color: '#00E396',
|
||||
data: this.format(this.stats.drive.remote.incSize)
|
||||
}, {
|
||||
name: 'Remote -',
|
||||
type: 'area',
|
||||
color: '#008FFB',
|
||||
color: '#FEB019',
|
||||
data: this.format(negate(this.stats.drive.remote.decSize))
|
||||
}]
|
||||
};
|
||||
|
@ -526,7 +566,9 @@ export default defineComponent({
|
|||
series: [{
|
||||
name: 'All',
|
||||
type: 'line',
|
||||
color: '#008FFB',
|
||||
color: '#09d8e2',
|
||||
borderDash: [5, 5],
|
||||
fill: false,
|
||||
data: this.format(
|
||||
sum(
|
||||
this.stats.drive.local.incCount,
|
||||
|
@ -543,17 +585,17 @@ export default defineComponent({
|
|||
}, {
|
||||
name: 'Local -',
|
||||
type: 'area',
|
||||
color: '#008FFB',
|
||||
color: '#FF4560',
|
||||
data: this.format(negate(this.stats.drive.local.decCount))
|
||||
}, {
|
||||
name: 'Remote +',
|
||||
type: 'area',
|
||||
color: '#008FFB',
|
||||
color: '#00E396',
|
||||
data: this.format(this.stats.drive.remote.incCount)
|
||||
}, {
|
||||
name: 'Remote -',
|
||||
type: 'area',
|
||||
color: '#008FFB',
|
||||
color: '#FEB019',
|
||||
data: this.format(negate(this.stats.drive.remote.decCount))
|
||||
}]
|
||||
};
|
||||
|
@ -589,17 +631,30 @@ export default defineComponent({
|
|||
|
||||
<style lang="scss" scoped>
|
||||
.zbcjwnqg {
|
||||
&.max-width_1200px {
|
||||
> .stats {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
margin: calc(0px - var(--margin) / 2);
|
||||
margin-bottom: calc(var(--margin) / 2);
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-template-rows: 1fr 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
&.max-width_550px {
|
||||
> .stats {
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: 1fr 1fr 1fr 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
> .stats {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr 1fr;
|
||||
grid-template-rows: 1fr;
|
||||
gap: var(--margin);
|
||||
margin-bottom: var(--margin);
|
||||
font-size: 90%;
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
flex: 1 0 213px;
|
||||
margin: calc(var(--margin) / 2);
|
||||
box-sizing: border-box;
|
||||
padding: 16px 20px;
|
||||
|
||||
|
@ -634,7 +689,7 @@ export default defineComponent({
|
|||
margin: 0;
|
||||
}
|
||||
|
||||
> dt {
|
||||
> dd {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div class="wrpstxzv" :class="{ children }" v-size="[{ max: 450 }]">
|
||||
<div class="wrpstxzv" :class="{ children }" v-size="{ max: [450] }">
|
||||
<div class="main">
|
||||
<mk-avatar class="avatar" :user="note.user"/>
|
||||
<div class="body">
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
:tabindex="!isDeleted ? '-1' : null"
|
||||
:class="{ renote: isRenote }"
|
||||
v-hotkey="keymap"
|
||||
v-size="[{ max: 500 }, { max: 450 }, { max: 350 }, { max: 300 }]"
|
||||
v-size="{ max: [500, 450, 350, 300] }"
|
||||
>
|
||||
<x-sub v-for="note in conversation" class="reply-to-more" :key="note.id" :note="note"/>
|
||||
<x-sub :note="appearNote.reply" class="reply-to" v-if="appearNote.reply"/>
|
||||
|
@ -57,6 +57,7 @@
|
|||
<mk-url-preview v-for="url in urls" :url="url" :key="url" :compact="true" :detail="detail" class="url-preview"/>
|
||||
<div class="renote" v-if="appearNote.renote"><x-note-preview :note="appearNote.renote"/></div>
|
||||
</div>
|
||||
<router-link v-if="appearNote.channel && !inChannel" class="channel" :to="`/channels/${appearNote.channel.id}`"><fa :icon="faSatelliteDish"/> {{ appearNote.channel.name }}</router-link>
|
||||
</div>
|
||||
<footer class="footer">
|
||||
<x-reactions-viewer :note="appearNote" ref="reactionsViewer"/>
|
||||
|
@ -96,7 +97,7 @@
|
|||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import { faBolt, faTimes, faBullhorn, faStar, faLink, faExternalLinkSquareAlt, faPlus, faMinus, faRetweet, faReply, faReplyAll, faEllipsisH, faHome, faUnlock, faEnvelope, faThumbtack, faBan, faQuoteRight, faInfoCircle, faBiohazard, faPlug } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faSatelliteDish, faBolt, faTimes, faBullhorn, faStar, faLink, faExternalLinkSquareAlt, faPlus, faMinus, faRetweet, faReply, faReplyAll, faEllipsisH, faHome, faUnlock, faEnvelope, faThumbtack, faBan, faQuoteRight, faInfoCircle, faBiohazard, faPlug } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faCopy, faTrashAlt, faEdit, faEye, faEyeSlash } from '@fortawesome/free-regular-svg-icons';
|
||||
import { parse } from '../../mfm/parse';
|
||||
import { sum, unique } from '../../prelude/array';
|
||||
|
@ -134,6 +135,12 @@ export default defineComponent({
|
|||
MkUrlPreview,
|
||||
},
|
||||
|
||||
inject: {
|
||||
inChannel: {
|
||||
default: null
|
||||
}
|
||||
},
|
||||
|
||||
props: {
|
||||
note: {
|
||||
type: Object,
|
||||
|
@ -160,7 +167,7 @@ export default defineComponent({
|
|||
isDeleted: false,
|
||||
muted: false,
|
||||
noteBody: this.$refs.noteBody,
|
||||
faEdit, faBolt, faTimes, faBullhorn, faPlus, faMinus, faRetweet, faReply, faReplyAll, faEllipsisH, faHome, faUnlock, faEnvelope, faThumbtack, faBan, faBiohazard, faPlug
|
||||
faEdit, faBolt, faTimes, faBullhorn, faPlus, faMinus, faRetweet, faReply, faReplyAll, faEllipsisH, faHome, faUnlock, faEnvelope, faThumbtack, faBan, faBiohazard, faPlug, faSatelliteDish
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -957,6 +964,11 @@ export default defineComponent({
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .channel {
|
||||
opacity: 0.7;
|
||||
font-size: 80%;
|
||||
}
|
||||
}
|
||||
|
||||
> .footer {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div class="qglefbjs" :class="notification.type" v-size="[{ max: 500 }, { max: 600 }]">
|
||||
<div class="qglefbjs" :class="notification.type" v-size="{ max: [500, 600] }">
|
||||
<div class="head">
|
||||
<mk-avatar v-if="notification.user" class="icon" :user="notification.user"/>
|
||||
<img v-else class="icon" :src="notification.icon" alt=""/>
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
<div>
|
||||
<span class="local-only" v-if="localOnly" v-text="$t('_visibility.localOnly')" />
|
||||
<span class="text-count" :class="{ over: trimmedLength(text) > max }">{{ max - trimmedLength(text) }}</span>
|
||||
<button class="_button visibility" @click="setVisibility" ref="visibilityButton" v-tooltip="$t('visibility')">
|
||||
<button class="_button visibility" @click="setVisibility" ref="visibilityButton" v-tooltip="$t('visibility')" :disabled="channel != null">
|
||||
<span v-if="visibility === 'public'"><fa :icon="faGlobe"/></span>
|
||||
<span v-if="visibility === 'home'"><fa :icon="faHome"/></span>
|
||||
<span v-if="visibility === 'followers'"><fa :icon="faUnlock"/></span>
|
||||
|
@ -88,6 +88,10 @@ export default defineComponent({
|
|||
type: Object,
|
||||
required: false
|
||||
},
|
||||
channel: {
|
||||
type: Object,
|
||||
required: false
|
||||
},
|
||||
mention: {
|
||||
type: Object,
|
||||
required: false
|
||||
|
@ -140,15 +144,28 @@ export default defineComponent({
|
|||
},
|
||||
|
||||
computed: {
|
||||
draftId(): string {
|
||||
return this.renote
|
||||
? `renote:${this.renote.id}`
|
||||
: this.reply
|
||||
? `reply:${this.reply.id}`
|
||||
: 'note';
|
||||
draftKey(): string {
|
||||
let key = this.channel ? `channel:${this.channel.id}` : '';
|
||||
|
||||
if (this.renote) {
|
||||
key += `renote:${this.renote.id}`;
|
||||
} else if (this.reply) {
|
||||
key += `reply:${this.reply.id}`;
|
||||
} else {
|
||||
key += 'note';
|
||||
}
|
||||
|
||||
return key;
|
||||
},
|
||||
|
||||
placeholder(): string {
|
||||
if (this.renote) {
|
||||
return this.$t('_postForm.quotePlaceholder');
|
||||
} else if (this.reply) {
|
||||
return this.$t('_postForm.replyPlaceholder');
|
||||
} else if (this.channel) {
|
||||
return this.$t('_postForm.channelPlaceholder');
|
||||
} else {
|
||||
const xs = [
|
||||
this.$t('_postForm._placeholders.a'),
|
||||
this.$t('_postForm._placeholders.b'),
|
||||
|
@ -157,13 +174,8 @@ export default defineComponent({
|
|||
this.$t('_postForm._placeholders.e'),
|
||||
this.$t('_postForm._placeholders.f')
|
||||
];
|
||||
const x = xs[Math.floor(Math.random() * xs.length)];
|
||||
|
||||
return this.renote
|
||||
? this.$t('_postForm.quotePlaceholder')
|
||||
: this.reply
|
||||
? this.$t('_postForm.replyPlaceholder')
|
||||
: x;
|
||||
return xs[Math.floor(Math.random() * xs.length)];
|
||||
}
|
||||
},
|
||||
|
||||
submitText(): string {
|
||||
|
@ -224,9 +236,11 @@ export default defineComponent({
|
|||
}
|
||||
|
||||
// デフォルト公開範囲
|
||||
if (this.channel == null) {
|
||||
this.applyVisibility(this.$store.state.settings.rememberNoteVisibility ? this.$store.state.deviceUser.visibility : this.$store.state.settings.defaultNoteVisibility);
|
||||
|
||||
this.localOnly = this.$store.state.settings.rememberNoteVisibility ? this.$store.state.deviceUser.localOnly : this.$store.state.settings.defaultNoteLocalOnly;
|
||||
}
|
||||
|
||||
// 公開以外へのリプライ時は元の公開範囲を引き継ぐ
|
||||
if (this.reply && ['home', 'followers', 'specified'].includes(this.reply.visibility)) {
|
||||
|
@ -266,7 +280,7 @@ export default defineComponent({
|
|||
this.$nextTick(() => {
|
||||
// 書きかけの投稿を復元
|
||||
if (!this.instant && !this.mention) {
|
||||
const draft = JSON.parse(localStorage.getItem('drafts') || '{}')[this.draftId];
|
||||
const draft = JSON.parse(localStorage.getItem('drafts') || '{}')[this.draftKey];
|
||||
if (draft) {
|
||||
this.text = draft.data.text;
|
||||
this.useCw = draft.data.useCw;
|
||||
|
@ -398,6 +412,10 @@ export default defineComponent({
|
|||
},
|
||||
|
||||
setVisibility() {
|
||||
if (this.channel) {
|
||||
// TODO: information dialog
|
||||
return;
|
||||
}
|
||||
const w = this.$root.new(MkVisibilityChooser, {
|
||||
source: this.$refs.visibilityButton,
|
||||
currentVisibility: this.visibility,
|
||||
|
@ -510,7 +528,7 @@ export default defineComponent({
|
|||
|
||||
const data = JSON.parse(localStorage.getItem('drafts') || '{}');
|
||||
|
||||
data[this.draftId] = {
|
||||
data[this.draftKey] = {
|
||||
updatedAt: new Date(),
|
||||
data: {
|
||||
text: this.text,
|
||||
|
@ -529,7 +547,7 @@ export default defineComponent({
|
|||
deleteDraft() {
|
||||
const data = JSON.parse(localStorage.getItem('drafts') || '{}');
|
||||
|
||||
delete data[this.draftId];
|
||||
delete data[this.draftKey];
|
||||
|
||||
localStorage.setItem('drafts', JSON.stringify(data));
|
||||
},
|
||||
|
@ -540,6 +558,7 @@ export default defineComponent({
|
|||
fileIds: this.files.length > 0 ? this.files.map(f => f.id) : undefined,
|
||||
replyId: this.reply ? this.reply.id : undefined,
|
||||
renoteId: this.renote ? this.renote.id : this.quoteId ? this.quoteId : undefined,
|
||||
channelId: this.channel ? this.channel.id : undefined,
|
||||
poll: this.poll ? (this.$refs.poll as any).get() : undefined,
|
||||
cw: this.useCw ? this.cw || '' : undefined,
|
||||
localOnly: this.localOnly,
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div class="nsbbhtug" v-if="hasDisconnected" @click="resetDisconnected">
|
||||
<div class="nsbbhtug" v-if="hasDisconnected && $store.state.device.serverDisconnectedBehavior === 'quiet'" @click="resetDisconnected">
|
||||
<div>{{ $t('disconnectedFromServer') }}</div>
|
||||
<div class="command">
|
||||
<button class="_textButton" @click="reload">{{ $t('reload') }}</button>
|
||||
|
@ -23,21 +23,12 @@ export default defineComponent({
|
|||
},
|
||||
},
|
||||
created() {
|
||||
this.$root.stream.on('_connected_', this.onConnected);
|
||||
this.$root.stream.on('_disconnected_', this.onDisconnected);
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.$root.stream.off('_connected_', this.onConnected);
|
||||
this.$root.stream.off('_disconnected_', this.onDisconnected);
|
||||
},
|
||||
methods: {
|
||||
onConnected() {
|
||||
if (this.hasDisconnected) {
|
||||
if (this.$store.state.device.autoReload) {
|
||||
this.reload();
|
||||
}
|
||||
}
|
||||
},
|
||||
onDisconnected() {
|
||||
this.hasDisconnected = true;
|
||||
},
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div class="pxhvhrfw" v-size="[{ max: 500 }]">
|
||||
<div class="pxhvhrfw" v-size="{ max: [500] }">
|
||||
<button v-for="item in items" class="_button" @click="$emit('input', item.value)" :class="{ active: value === item.value }" :key="item.value"><fa v-if="item.icon" :icon="item.icon" class="icon"/>{{ item.label }}</button>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -24,6 +24,10 @@ export default defineComponent({
|
|||
type: String,
|
||||
required: false
|
||||
},
|
||||
channel: {
|
||||
type: String,
|
||||
required: false
|
||||
},
|
||||
sound: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
|
@ -31,6 +35,12 @@ export default defineComponent({
|
|||
}
|
||||
},
|
||||
|
||||
provide() {
|
||||
return {
|
||||
inChannel: this.src === 'channel'
|
||||
};
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
connection: null,
|
||||
|
@ -112,6 +122,15 @@ export default defineComponent({
|
|||
this.connection.on('note', prepend);
|
||||
this.connection.on('userAdded', onUserAdded);
|
||||
this.connection.on('userRemoved', onUserRemoved);
|
||||
} else if (this.src == 'channel') {
|
||||
endpoint = 'channels/timeline';
|
||||
this.query = {
|
||||
channelId: this.channel
|
||||
};
|
||||
this.connection = this.$root.stream.connectToChannel('channel', {
|
||||
channelId: this.channel
|
||||
});
|
||||
this.connection.on('note', prepend);
|
||||
}
|
||||
|
||||
this.pagination = {
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
<template>
|
||||
<div class="ukygtjoj _panel" :class="{ naked, hideHeader: !showHeader, scrollable }" v-size="[{ max: 500 }]">
|
||||
<header v-if="showHeader">
|
||||
<div class="ukygtjoj _panel" :class="{ naked, hideHeader: !showHeader, scrollable, closed: !showBody }" v-size="{ max: [380], el: resizeBaseEl }">
|
||||
<header v-if="showHeader" ref="header">
|
||||
<div class="title"><slot name="header"></slot></div>
|
||||
<div class="sub">
|
||||
<slot name="func"></slot>
|
||||
<button class="_button" v-if="bodyTogglable" @click="() => showBody = !showBody">
|
||||
<template v-if="showBody"><fa :icon="faAngleUp"/></template>
|
||||
<template v-else><fa :icon="faAngleDown"/></template>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
<transition name="container-toggle"
|
||||
@enter="enter"
|
||||
|
@ -52,6 +54,9 @@ export default defineComponent({
|
|||
required: false,
|
||||
default: false
|
||||
},
|
||||
resizeBaseEl: {
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
@ -59,6 +64,18 @@ export default defineComponent({
|
|||
faAngleUp, faAngleDown
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.$watch('showBody', showBody => {
|
||||
this.$el.style.minHeight = `${this.$refs.header.offsetHeight}px`;
|
||||
if (showBody) {
|
||||
this.$el.style.flexBasis = `auto`;
|
||||
} else {
|
||||
this.$el.style.flexBasis = `${this.$refs.header.offsetHeight}px`;
|
||||
}
|
||||
}, {
|
||||
immediate: true
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
toggleContent(show: boolean) {
|
||||
if (!this.bodyTogglable) return;
|
||||
|
@ -103,10 +120,6 @@ export default defineComponent({
|
|||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
& + .ukygtjoj {
|
||||
margin-top: var(--margin);
|
||||
}
|
||||
|
||||
&.naked {
|
||||
background: transparent !important;
|
||||
box-shadow: none !important;
|
||||
|
@ -127,6 +140,7 @@ export default defineComponent({
|
|||
z-index: 2;
|
||||
background: var(--panelHeaderBg);
|
||||
color: var(--panelHeaderFg);
|
||||
line-height: 1.4em;
|
||||
|
||||
> .title {
|
||||
margin: 0;
|
||||
|
@ -141,23 +155,42 @@ export default defineComponent({
|
|||
}
|
||||
}
|
||||
|
||||
> button {
|
||||
> .sub {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
top: 0;
|
||||
right: 0;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
|
||||
> button {
|
||||
width: 42px;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.max-width_500px {
|
||||
> div {
|
||||
> ::v-deep ._content {
|
||||
padding: 24px;
|
||||
|
||||
& + ._content {
|
||||
border-top: solid 1px var(--divider);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.max-width_380px {
|
||||
> header {
|
||||
> .title {
|
||||
padding: 8px 10px;
|
||||
}
|
||||
}
|
||||
|
||||
> div {
|
||||
> ::v-deep ._content {
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,126 @@
|
|||
<template>
|
||||
<div class="ssazuxis" v-size="{ max: [500] }">
|
||||
<header @click="() => showBody = !showBody" class="_button">
|
||||
<div class="title"><slot name="header"></slot></div>
|
||||
<div class="divider"></div>
|
||||
<button class="_button">
|
||||
<template v-if="showBody"><fa :icon="faAngleUp"/></template>
|
||||
<template v-else><fa :icon="faAngleDown"/></template>
|
||||
</button>
|
||||
</header>
|
||||
<transition name="folder-toggle"
|
||||
@enter="enter"
|
||||
@after-enter="afterEnter"
|
||||
@leave="leave"
|
||||
@after-leave="afterLeave"
|
||||
>
|
||||
<div v-show="showBody">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { faAngleUp, faAngleDown } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
export default Vue.extend({
|
||||
props: {
|
||||
expanded: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showBody: this.expanded,
|
||||
faAngleUp, faAngleDown
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
toggleContent(show: boolean) {
|
||||
this.showBody = show;
|
||||
},
|
||||
|
||||
enter(el) {
|
||||
const elementHeight = el.getBoundingClientRect().height;
|
||||
el.style.height = 0;
|
||||
el.offsetHeight; // reflow
|
||||
el.style.height = elementHeight + 'px';
|
||||
},
|
||||
afterEnter(el) {
|
||||
el.style.height = null;
|
||||
},
|
||||
leave(el) {
|
||||
const elementHeight = el.getBoundingClientRect().height;
|
||||
el.style.height = elementHeight + 'px';
|
||||
el.offsetHeight; // reflow
|
||||
el.style.height = 0;
|
||||
},
|
||||
afterLeave(el) {
|
||||
el.style.height = null;
|
||||
},
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.folder-toggle-enter-active, .folder-toggle-leave-active {
|
||||
overflow-y: hidden;
|
||||
transition: opacity 0.5s, height 0.5s !important;
|
||||
}
|
||||
.folder-toggle-enter {
|
||||
opacity: 0;
|
||||
}
|
||||
.folder-toggle-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.ssazuxis {
|
||||
position: relative;
|
||||
|
||||
> header {
|
||||
display: flex;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
// TODO
|
||||
// position: sticky;
|
||||
// top: var(--stickyTopOffset);
|
||||
// backdrop-filter: blur(20px);
|
||||
|
||||
> .title {
|
||||
margin: 0;
|
||||
padding: 12px 16px 12px 8px;
|
||||
|
||||
> [data-icon] {
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
&:empty {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
> .divider {
|
||||
flex: 1;
|
||||
margin: auto;
|
||||
height: 1px;
|
||||
background: var(--divider);
|
||||
}
|
||||
|
||||
> button {
|
||||
width: 42px;
|
||||
}
|
||||
}
|
||||
|
||||
&.max-width_500px {
|
||||
> header {
|
||||
> .title {
|
||||
padding: 8px 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -64,7 +64,7 @@
|
|||
<div class="suffix" ref="suffix"><slot name="suffix"></slot></div>
|
||||
</div>
|
||||
<button class="save _textButton" v-if="save && changed" @click="() => { changed = false; save(); }">{{ $t('save') }}</button>
|
||||
<div class="desc"><slot name="desc"></slot></div>
|
||||
<div class="desc _caption"><slot name="desc"></slot></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -401,13 +401,11 @@ export default defineComponent({
|
|||
|
||||
> .save {
|
||||
margin: 6px 0 0 0;
|
||||
font-size: 13px;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
> .desc {
|
||||
margin: 6px 0 0 0;
|
||||
font-size: 13px;
|
||||
opacity: 0.7;
|
||||
|
||||
&:empty {
|
||||
display: none;
|
||||
|
|
|
@ -196,7 +196,7 @@ export default defineComponent({
|
|||
|
||||
> .text {
|
||||
margin: 6px 0;
|
||||
font-size: 13px;
|
||||
font-size: 0.8em;
|
||||
|
||||
&:empty {
|
||||
display: none;
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
></textarea>
|
||||
</div>
|
||||
<button class="save _textButton" v-if="save && changed" @click="() => { changed = false; save(); }">{{ $t('save') }}</button>
|
||||
<div class="desc"><slot name="desc"></slot></div>
|
||||
<div class="desc _caption"><slot name="desc"></slot></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -163,13 +163,11 @@ export default defineComponent({
|
|||
|
||||
> .save {
|
||||
margin: 6px 0 0 0;
|
||||
font-size: 13px;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
> .desc {
|
||||
margin: 6px 0 0 0;
|
||||
font-size: 13px;
|
||||
opacity: 0.7;
|
||||
|
||||
&:empty {
|
||||
display: none;
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
<div v-else-if="tweetId && tweetExpanded" class="twitter" ref="twitter">
|
||||
<iframe ref="tweet" scrolling="no" frameborder="no" :style="{ position: 'relative', left: `${tweetLeft}px`, width: `${tweetLeft < 0 ? 'auto' : '100%'}`, height: `${tweetHeight}px` }" :src="`https://platform.twitter.com/embed/index.html?embedId=${embedId}&hideCard=false&hideThread=false&lang=en&theme=${$store.state.device.darkMode ? 'dark' : 'light'}&id=${tweetId}`"></iframe>
|
||||
</div>
|
||||
<div v-else class="mk-url-preview" v-size="[{ max: 400 }, { max: 350 }]">
|
||||
<div v-else class="mk-url-preview" v-size="{ max: [400, 350] }">
|
||||
<transition name="zoom" mode="out-in">
|
||||
<component :is="self ? 'router-link' : 'a'" :class="{ compact }" :[attr]="self ? url.substr(local.length) : url" rel="nofollow noopener" :target="target" :title="url" v-if="!fetching">
|
||||
<div class="thumbnail" v-if="thumbnail" :style="`background-image: url('${thumbnail}')`">
|
||||
|
|
|
@ -33,7 +33,7 @@
|
|||
|
||||
<x-sidebar ref="nav" @change-view-mode="calcHeaderWidth"/>
|
||||
|
||||
<div class="contents" ref="contents" :class="{ wallpaper }">
|
||||
<div class="contents" ref="contents" :class="{ wallpaper, full: $store.state.fullView }">
|
||||
<main ref="main">
|
||||
<div class="content">
|
||||
<router-view v-slot="{ Component }">
|
||||
|
@ -549,6 +549,18 @@ export default defineComponent({
|
|||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
&.full {
|
||||
width: 100%;
|
||||
|
||||
> main {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
> .widgets {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
> main {
|
||||
width: $main-width;
|
||||
min-width: 0;
|
||||
|
|
|
@ -3,27 +3,12 @@ import { Directive } from 'vue';
|
|||
//const observers = new Map<Element, ResizeObserver>();
|
||||
|
||||
export default {
|
||||
mounted(el, binding, vn) {
|
||||
mounted(src, binding, vn) {
|
||||
const query = binding.value;
|
||||
|
||||
/*
|
||||
const addClassRecursive = (el: Element, cls: string) => {
|
||||
el.classList.add(cls);
|
||||
if (el.children) {
|
||||
for (const child of el.children) {
|
||||
addClassRecursive(child, cls);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const removeClassRecursive = (el: Element, cls: string) => {
|
||||
el.classList.remove(cls);
|
||||
if (el.children) {
|
||||
for (const child of el.children) {
|
||||
removeClassRecursive(child, cls);
|
||||
}
|
||||
}
|
||||
};*/
|
||||
// TODO: 要素をもらうというよりはカスタム幅算出関数をもらうようにしてcalcで都度呼び出して計算するようにした方が柔軟そう
|
||||
// その場合はunbindの方も改修することを忘れずに
|
||||
const el = query.el ? query.el() : src;
|
||||
|
||||
const addClass = (el: Element, cls: string) => {
|
||||
el.classList.add(cls);
|
||||
|
@ -36,19 +21,21 @@ export default {
|
|||
const calc = () => {
|
||||
const width = el.clientWidth;
|
||||
|
||||
for (const q of query) {
|
||||
if (q.max) {
|
||||
if (width <= q.max) {
|
||||
addClass(el, 'max-width_' + q.max + 'px');
|
||||
if (query.max) {
|
||||
for (const v of query.max) {
|
||||
if (width <= v) {
|
||||
addClass(src, 'max-width_' + v + 'px');
|
||||
} else {
|
||||
removeClass(el, 'max-width_' + q.max + 'px');
|
||||
removeClass(src, 'max-width_' + v + 'px');
|
||||
}
|
||||
}
|
||||
if (q.min) {
|
||||
if (width >= q.min) {
|
||||
addClass(el, 'min-width_' + q.min + 'px');
|
||||
}
|
||||
if (query.min) {
|
||||
for (const v of query.min) {
|
||||
if (width >= v) {
|
||||
addClass(src, 'min-width_' + v + 'px');
|
||||
} else {
|
||||
removeClass(el, 'min-width_' + q.min + 'px');
|
||||
removeClass(src, 'min-width_' + v + 'px');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -71,7 +58,11 @@ export default {
|
|||
el._ro_ = ro;
|
||||
},
|
||||
|
||||
unmounted(el, binding, vn) {
|
||||
unmounted(src, binding, vn) {
|
||||
const query = binding.value;
|
||||
|
||||
const el = query.el ? query.el() : src;
|
||||
|
||||
el._ro_.unobserve(el);
|
||||
}
|
||||
} as Directive;
|
||||
|
|
|
@ -217,6 +217,25 @@ store.watch(state => state.device.useBlurEffectForModal, v => {
|
|||
document.documentElement.style.setProperty('--modalBgFilter', v ? 'blur(4px)' : 'none');
|
||||
}, { immediate: true });
|
||||
|
||||
let reloadDialogShowing = false;
|
||||
stream.on('_disconnected_', async () => {
|
||||
if (store.state.device.serverDisconnectedBehavior === 'reload') {
|
||||
location.reload();
|
||||
} else if (store.state.device.serverDisconnectedBehavior === 'dialog') {
|
||||
if (reloadDialogShowing) return;
|
||||
reloadDialogShowing = true;
|
||||
const { canceled } = await app.dialog({
|
||||
type: 'warning',
|
||||
title: app.$t('disconnectedFromServer'),
|
||||
text: app.$t('reloadConfirm'),
|
||||
showCancelButton: true
|
||||
});
|
||||
reloadDialogShowing = false;
|
||||
if (!canceled) {
|
||||
location.reload();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
stream.on('emojiAdded', data => {
|
||||
// TODO
|
||||
|
@ -337,6 +356,26 @@ if (store.getters.isSignedIn) {
|
|||
});
|
||||
});
|
||||
|
||||
main.on('readAllChannels', () => {
|
||||
store.dispatch('mergeMe', {
|
||||
hasUnreadChannel: false
|
||||
});
|
||||
});
|
||||
|
||||
main.on('unreadChannel', () => {
|
||||
store.dispatch('mergeMe', {
|
||||
hasUnreadChannel: true
|
||||
});
|
||||
|
||||
app.sound('channel');
|
||||
});
|
||||
|
||||
main.on('readAllAnnouncements', () => {
|
||||
store.dispatch('mergeMe', {
|
||||
hasUnreadAnnouncement: false
|
||||
});
|
||||
});
|
||||
|
||||
main.on('clientSettingUpdated', x => {
|
||||
store.commit('settings/set', {
|
||||
key: x.key,
|
||||
|
|
|
@ -0,0 +1,128 @@
|
|||
<template>
|
||||
<div>
|
||||
<portal to="icon"><fa :icon="faSatelliteDish"/></portal>
|
||||
<portal to="title">{{ channelId ? $t('_channel.edit') : $t('_channel.create') }}</portal>
|
||||
|
||||
<div class="_card">
|
||||
<div class="_content">
|
||||
<mk-input v-model="name">{{ $t('name') }}</mk-input>
|
||||
|
||||
<mk-textarea v-model="description">{{ $t('description') }}</mk-textarea>
|
||||
|
||||
<div class="banner">
|
||||
<mk-button v-if="bannerId == null" @click="setBannerImage"><fa :icon="faPlus"/> {{ $t('_channel.setBanner') }}</mk-button>
|
||||
<div v-else-if="bannerUrl">
|
||||
<img :src="bannerUrl" style="width: 100%;"/>
|
||||
<mk-button @click="removeBannerImage()"><fa :icon="faTrashAlt"/> {{ $t('_channel.removeBanner') }}</mk-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="_footer">
|
||||
<mk-button @click="save()" primary><fa :icon="faSave"/> {{ channelId ? $t('save') : $t('create') }}</mk-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { faPlus, faSatelliteDish } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faSave, faTrashAlt } from '@fortawesome/free-regular-svg-icons';
|
||||
import MkTextarea from '../components/ui/textarea.vue';
|
||||
import MkButton from '../components/ui/button.vue';
|
||||
import MkInput from '../components/ui/input.vue';
|
||||
import { selectFile } from '../scripts/select-file';
|
||||
|
||||
export default Vue.extend({
|
||||
components: {
|
||||
MkTextarea, MkButton, MkInput,
|
||||
},
|
||||
|
||||
props: {
|
||||
channelId: {
|
||||
type: String,
|
||||
required: false
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
channel: null,
|
||||
name: null,
|
||||
description: null,
|
||||
bannerUrl: null,
|
||||
bannerId: null,
|
||||
faSave, faTrashAlt, faPlus,faSatelliteDish,
|
||||
};
|
||||
},
|
||||
|
||||
watch: {
|
||||
async bannerId() {
|
||||
if (this.bannerId == null) {
|
||||
this.bannerUrl = null;
|
||||
} else {
|
||||
this.bannerUrl = (await this.$root.api('drive/files/show', {
|
||||
fileId: this.bannerId,
|
||||
})).url;
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
async created() {
|
||||
if (this.channelId) {
|
||||
this.channel = await this.$root.api('channels/show', {
|
||||
channelId: this.channelId,
|
||||
});
|
||||
|
||||
this.name = this.channel.name;
|
||||
this.description = this.channel.description;
|
||||
this.bannerId = this.channel.bannerId;
|
||||
this.bannerUrl = this.channel.bannerUrl;
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
save() {
|
||||
const params = {
|
||||
name: this.name,
|
||||
description: this.description,
|
||||
bannerId: this.bannerId,
|
||||
};
|
||||
|
||||
if (this.channelId) {
|
||||
params.channelId = this.channelId;
|
||||
this.$root.api('channels/update', params)
|
||||
.then(channel => {
|
||||
this.$root.dialog({
|
||||
type: 'success',
|
||||
iconOnly: true, autoClose: true
|
||||
});
|
||||
});
|
||||
} else {
|
||||
this.$root.api('channels/create', params)
|
||||
.then(channel => {
|
||||
this.$root.dialog({
|
||||
type: 'success',
|
||||
iconOnly: true, autoClose: true
|
||||
});
|
||||
this.$router.push(`/channels/${channel.id}`);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
setBannerImage(e) {
|
||||
selectFile(this, e.currentTarget || e.target, null, false).then(file => {
|
||||
this.bannerId = file.id;
|
||||
});
|
||||
},
|
||||
|
||||
removeBannerImage() {
|
||||
this.bannerId = null;
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
|
@ -0,0 +1,190 @@
|
|||
<template>
|
||||
<div v-if="channel">
|
||||
<portal to="icon"><fa :icon="faSatelliteDish"/></portal>
|
||||
<portal to="title">{{ channel.name }}</portal>
|
||||
|
||||
<div class="wpgynlbz _panel _vMargin" :class="{ hide: !showBanner }">
|
||||
<x-channel-follow-button :channel="channel" :full="true" class="subscribe"/>
|
||||
<button class="_button toggle" @click="() => showBanner = !showBanner">
|
||||
<template v-if="showBanner"><fa :icon="faAngleUp"/></template>
|
||||
<template v-else><fa :icon="faAngleDown"/></template>
|
||||
</button>
|
||||
<div class="hideOverlay" v-if="!showBanner">
|
||||
</div>
|
||||
<div :style="{ backgroundImage: channel.bannerUrl ? `url(${channel.bannerUrl})` : null }" class="banner">
|
||||
<div class="status">
|
||||
<div><fa :icon="faUsers" fixed-width/><i18n path="_channel.usersCount" tag="span" style="margin-left: 4px;"><b place="n">{{ channel.usersCount }}</b></i18n></div>
|
||||
<div><fa :icon="faPencilAlt" fixed-width/><i18n path="_channel.notesCount" tag="span" style="margin-left: 4px;"><b place="n">{{ channel.notesCount }}</b></i18n></div>
|
||||
</div>
|
||||
<div class="fade"></div>
|
||||
</div>
|
||||
<div class="description" v-if="channel.description">
|
||||
<mfm :text="channel.description" :is-note="false" :i="$store.state.i"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<x-post-form :channel="channel" class="post-form _panel _vMargin" fixed/>
|
||||
|
||||
<x-timeline class="_vMargin" src="channel" :channel="channelId" @before="before" @after="after"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { faSatelliteDish, faUsers, faPencilAlt, faAngleUp, faAngleDown } from '@fortawesome/free-solid-svg-icons';
|
||||
import { } from '@fortawesome/free-regular-svg-icons';
|
||||
import MkContainer from '../components/ui/container.vue';
|
||||
import XPostForm from '../components/post-form.vue';
|
||||
import XTimeline from '../components/timeline.vue';
|
||||
import XChannelFollowButton from '../components/channel-follow-button.vue';
|
||||
|
||||
export default Vue.extend({
|
||||
metaInfo() {
|
||||
return {
|
||||
title: this.$t('channel') as string
|
||||
};
|
||||
},
|
||||
|
||||
components: {
|
||||
MkContainer,
|
||||
XPostForm,
|
||||
XTimeline,
|
||||
XChannelFollowButton
|
||||
},
|
||||
|
||||
props: {
|
||||
channelId: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
channel: null,
|
||||
showBanner: true,
|
||||
pagination: {
|
||||
endpoint: 'channels/timeline',
|
||||
limit: 10,
|
||||
params: () => ({
|
||||
channelId: this.channelId,
|
||||
})
|
||||
},
|
||||
faSatelliteDish, faUsers, faPencilAlt, faAngleUp, faAngleDown,
|
||||
};
|
||||
},
|
||||
|
||||
watch: {
|
||||
channelId: {
|
||||
async handler() {
|
||||
this.channel = await this.$root.api('channels/show', {
|
||||
channelId: this.channelId,
|
||||
});
|
||||
},
|
||||
immediate: true
|
||||
}
|
||||
},
|
||||
|
||||
created() {
|
||||
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.wpgynlbz {
|
||||
> .subscribe {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
top: 16px;
|
||||
left: 16px;
|
||||
}
|
||||
|
||||
> .toggle {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
font-size: 1.2em;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
color: #fff;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
border-radius: 100%;
|
||||
|
||||
> [data-icon] {
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
> .banner {
|
||||
position: relative;
|
||||
height: 200px;
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
|
||||
> .fade {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 64px;
|
||||
background: linear-gradient(0deg, var(--panel), var(--X15));
|
||||
}
|
||||
|
||||
> .status {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
bottom: 16px;
|
||||
right: 16px;
|
||||
padding: 8px 12px;
|
||||
font-size: 80%;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
border-radius: 6px;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
> .description {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
> .hideOverlay {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
-webkit-backdrop-filter: blur(16px);
|
||||
backdrop-filter: blur(16px);
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
&.hide {
|
||||
> .subscribe {
|
||||
display: none;
|
||||
}
|
||||
|
||||
> .toggle {
|
||||
top: 0;
|
||||
right: 0;
|
||||
height: 100%;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
> .banner {
|
||||
height: 42px;
|
||||
filter: blur(8px);
|
||||
|
||||
> * {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
> .description {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,86 @@
|
|||
<template>
|
||||
<div>
|
||||
<portal to="icon"><fa :icon="faSatelliteDish"/></portal>
|
||||
<portal to="title">{{ $t('channel') }}</portal>
|
||||
|
||||
<mk-tab v-model="tab" :items="[{ label: $t('_channel.featured'), value: 'featured', icon: faFireAlt }, { label: $t('_channel.following'), value: 'following', icon: faHeart }, { label: $t('_channel.owned'), value: 'owned', icon: faEdit }]"/>
|
||||
|
||||
<div class="grwlizim featured" v-if="tab === 'featured'">
|
||||
<mk-pagination :pagination="featuredPagination" #default="{items}">
|
||||
<mk-channel-preview v-for="channel in items" class="uveselbe" :channel="channel" :key="channel.id"/>
|
||||
</mk-pagination>
|
||||
</div>
|
||||
|
||||
<div class="grwlizim following" v-if="tab === 'following'">
|
||||
<mk-pagination :pagination="followingPagination" #default="{items}">
|
||||
<mk-channel-preview v-for="channel in items" class="uveselbe" :channel="channel" :key="channel.id"/>
|
||||
</mk-pagination>
|
||||
</div>
|
||||
|
||||
<div class="grwlizim owned" v-if="tab === 'owned'">
|
||||
<mk-button class="new" @click="create()"><fa :icon="faPlus"/></mk-button>
|
||||
<mk-pagination :pagination="ownedPagination" #default="{items}">
|
||||
<mk-channel-preview v-for="channel in items" class="uveselbe" :channel="channel" :key="channel.id"/>
|
||||
</mk-pagination>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { faSatelliteDish, faPlus, faEdit, faFireAlt } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faHeart } from '@fortawesome/free-regular-svg-icons';
|
||||
import MkChannelPreview from '../components/channel-preview.vue';
|
||||
import MkPagination from '../components/ui/pagination.vue';
|
||||
import MkButton from '../components/ui/button.vue';
|
||||
import MkTab from '../components/tab.vue';
|
||||
|
||||
export default Vue.extend({
|
||||
components: {
|
||||
MkChannelPreview, MkPagination, MkButton, MkTab
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
tab: 'featured',
|
||||
featuredPagination: {
|
||||
endpoint: 'channels/featured',
|
||||
limit: 5,
|
||||
},
|
||||
followingPagination: {
|
||||
endpoint: 'channels/followed',
|
||||
limit: 5,
|
||||
},
|
||||
ownedPagination: {
|
||||
endpoint: 'channels/owned',
|
||||
limit: 5,
|
||||
},
|
||||
faSatelliteDish, faPlus, faEdit, faHeart, faFireAlt
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
create() {
|
||||
this.$router.push(`/channels/new`);
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.grwlizim {
|
||||
padding: 16px 0;
|
||||
|
||||
&.my .uveselbe:first-child {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.uveselbe:not(:last-child) {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
@media (min-width: 500px) {
|
||||
.uveselbe:not(:last-child) {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -2,14 +2,15 @@
|
|||
<div class="mk-home" v-hotkey.global="keymap">
|
||||
<portal to="header" v-if="showTitle">
|
||||
<button @click="choose" class="_button _kjvfvyph_">
|
||||
<i><fa v-if="$store.state.i.hasUnreadAntenna" :icon="faCircle"/></i>
|
||||
<i><fa v-if="$store.state.i.hasUnreadAntenna || $store.state.i.hasUnreadChannel" :icon="faCircle"/></i>
|
||||
<fa v-if="src === 'home'" :icon="faHome"/>
|
||||
<fa v-if="src === 'local'" :icon="faComments"/>
|
||||
<fa v-if="src === 'social'" :icon="faShareAlt"/>
|
||||
<fa v-if="src === 'global'" :icon="faGlobe"/>
|
||||
<fa v-if="src === 'list'" :icon="faListUl"/>
|
||||
<fa v-if="src === 'antenna'" :icon="faSatellite"/>
|
||||
<span style="margin-left: 8px;">{{ src === 'list' ? list.name : src === 'antenna' ? antenna.name : $t('_timelines.' + src) }}</span>
|
||||
<fa v-if="src === 'channel'" :icon="faSatelliteDish"/>
|
||||
<span style="margin-left: 8px;">{{ src === 'list' ? list.name : src === 'antenna' ? antenna.name : src === 'channel' ? channel.name : $t('_timelines.' + src) }}</span>
|
||||
<fa :icon="menuOpened ? faAngleUp : faAngleDown" style="margin-left: 8px;"/>
|
||||
</button>
|
||||
</portal>
|
||||
|
@ -19,13 +20,13 @@
|
|||
<x-tutorial class="tutorial" v-if="$store.state.settings.tutorial != -1"/>
|
||||
|
||||
<x-post-form class="post-form _panel" fixed v-if="$store.state.device.showFixedPostForm"/>
|
||||
<x-timeline ref="tl" :key="src === 'list' ? `list:${list.id}` : src === 'antenna' ? `antenna:${antenna.id}` : src" :src="src" :list="list ? list.id : null" :antenna="antenna ? antenna.id : null" :sound="true" @before="before()" @after="after()" @queue="queueUpdated"/>
|
||||
<x-timeline ref="tl" :key="src === 'list' ? `list:${list.id}` : src === 'antenna' ? `antenna:${antenna.id}` : src === 'channel' ? `channel:${channel.id}` : src" :src="src" :list="list ? list.id : null" :antenna="antenna ? antenna.id : null" :channel="channel ? channel.id : null" :sound="true" @before="before()" @after="after()" @queue="queueUpdated"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, defineAsyncComponent } from 'vue';
|
||||
import { faAngleDown, faAngleUp, faHome, faShareAlt, faGlobe, faListUl, faSatellite, faCircle } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faAngleDown, faAngleUp, faHome, faShareAlt, faGlobe, faListUl, faSatellite, faSatelliteDish, faCircle } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faComments } from '@fortawesome/free-regular-svg-icons';
|
||||
import Progress from '../scripts/loading';
|
||||
import XTimeline from '../components/timeline.vue';
|
||||
|
@ -57,10 +58,11 @@ export default defineComponent({
|
|||
src: 'home',
|
||||
list: null,
|
||||
antenna: null,
|
||||
channel: null,
|
||||
menuOpened: false,
|
||||
queue: 0,
|
||||
width: 0,
|
||||
faAngleDown, faAngleUp, faHome, faShareAlt, faGlobe, faComments, faListUl, faSatellite, faCircle
|
||||
faAngleDown, faAngleUp, faHome, faShareAlt, faGlobe, faComments, faListUl, faSatellite, faSatelliteDish, faCircle
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -79,16 +81,20 @@ export default defineComponent({
|
|||
watch: {
|
||||
src() {
|
||||
this.showNav = false;
|
||||
this.saveSrc();
|
||||
},
|
||||
list(x) {
|
||||
this.showNav = false;
|
||||
this.saveSrc();
|
||||
if (x != null) this.antenna = null;
|
||||
if (x != null) this.channel = null;
|
||||
},
|
||||
antenna(x) {
|
||||
this.showNav = false;
|
||||
this.saveSrc();
|
||||
if (x != null) this.list = null;
|
||||
if (x != null) this.channel = null;
|
||||
},
|
||||
channel(x) {
|
||||
this.showNav = false;
|
||||
if (x != null) this.antenna = null;
|
||||
if (x != null) this.list = null;
|
||||
},
|
||||
},
|
||||
|
@ -99,6 +105,8 @@ export default defineComponent({
|
|||
this.list = this.$store.state.deviceUser.tl.arg;
|
||||
} else if (this.src === 'antenna') {
|
||||
this.antenna = this.$store.state.deviceUser.tl.arg;
|
||||
} else if (this.src === 'channel') {
|
||||
this.channel = this.$store.state.deviceUser.tl.arg;
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -127,9 +135,10 @@ export default defineComponent({
|
|||
async choose(ev) {
|
||||
if (this.meta == null) return;
|
||||
this.menuOpened = true;
|
||||
const [antennas, lists] = await Promise.all([
|
||||
const [antennas, lists, channels] = await Promise.all([
|
||||
this.$root.api('antennas/list'),
|
||||
this.$root.api('users/lists/list')
|
||||
this.$root.api('users/lists/list'),
|
||||
this.$root.api('channels/followed'),
|
||||
]);
|
||||
const antennaItems = antennas.map(antenna => ({
|
||||
text: antenna.name,
|
||||
|
@ -137,7 +146,8 @@ export default defineComponent({
|
|||
indicate: antenna.hasUnreadNote,
|
||||
action: () => {
|
||||
this.antenna = antenna;
|
||||
this.setSrc('antenna');
|
||||
this.src = 'antenna';
|
||||
this.saveSrc();
|
||||
}
|
||||
}));
|
||||
const listItems = lists.map(list => ({
|
||||
|
@ -145,27 +155,40 @@ export default defineComponent({
|
|||
icon: faListUl,
|
||||
action: () => {
|
||||
this.list = list;
|
||||
this.setSrc('list');
|
||||
this.src = 'list';
|
||||
this.saveSrc();
|
||||
}
|
||||
}));
|
||||
const channelItems = channels.map(channel => ({
|
||||
text: channel.name,
|
||||
icon: faSatelliteDish,
|
||||
indicate: channel.hasUnreadNote,
|
||||
action: () => {
|
||||
// NOTE: チャンネルタイムラインをこのコンポーネントで表示するようにすると投稿フォームはどうするかなどの問題が生じるのでとりあえずページ遷移で
|
||||
//this.channel = channel;
|
||||
//this.src = 'channel';
|
||||
//this.saveSrc();
|
||||
this.$router.push(`/channels/${channel.id}`);
|
||||
}
|
||||
}));
|
||||
this.$root.menu({
|
||||
items: [{
|
||||
text: this.$t('_timelines.home'),
|
||||
icon: faHome,
|
||||
action: () => { this.setSrc('home') }
|
||||
action: () => { this.src = 'home'; this.saveSrc(); }
|
||||
}, this.meta.disableLocalTimeline && !this.$store.state.i.isModerator && !this.$store.state.i.isAdmin ? undefined : {
|
||||
text: this.$t('_timelines.local'),
|
||||
icon: faComments,
|
||||
action: () => { this.setSrc('local') }
|
||||
action: () => { this.src = 'local'; this.saveSrc(); }
|
||||
}, this.meta.disableLocalTimeline && !this.$store.state.i.isModerator && !this.$store.state.i.isAdmin ? undefined : {
|
||||
text: this.$t('_timelines.social'),
|
||||
icon: faShareAlt,
|
||||
action: () => { this.setSrc('social') }
|
||||
action: () => { this.src = 'social'; this.saveSrc(); }
|
||||
}, this.meta.disableGlobalTimeline && !this.$store.state.i.isModerator && !this.$store.state.i.isAdmin ? undefined : {
|
||||
text: this.$t('_timelines.global'),
|
||||
icon: faGlobe,
|
||||
action: () => { this.setSrc('global') }
|
||||
}, antennaItems.length > 0 ? null : undefined, ...antennaItems, listItems.length > 0 ? null : undefined, ...listItems],
|
||||
action: () => { this.src = 'global'; this.saveSrc(); }
|
||||
}, antennaItems.length > 0 ? null : undefined, ...antennaItems, listItems.length > 0 ? null : undefined, ...listItems, channelItems.length > 0 ? null : undefined, ...channelItems],
|
||||
fixed: true,
|
||||
noCenter: true,
|
||||
source: ev.currentTarget || ev.target
|
||||
|
@ -174,14 +197,13 @@ export default defineComponent({
|
|||
});
|
||||
},
|
||||
|
||||
setSrc(src) {
|
||||
this.src = src;
|
||||
},
|
||||
|
||||
saveSrc() {
|
||||
this.$store.commit('deviceUser/setTl', {
|
||||
src: this.src,
|
||||
arg: this.src == 'list' ? this.list : this.antenna
|
||||
arg:
|
||||
this.src === 'list' ? this.list :
|
||||
this.src === 'antenna' ? this.antenna :
|
||||
this.channel
|
||||
});
|
||||
},
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<portal to="icon"><fa :icon="faLaugh"/></portal>
|
||||
<portal to="title">{{ $t('customEmojis') }}</portal>
|
||||
|
||||
<section class="_card local">
|
||||
<section class="_card _vMargin local">
|
||||
<div class="_title"><fa :icon="faLaugh"/> {{ $t('customEmojis') }}</div>
|
||||
<div class="_content">
|
||||
<mk-pagination :pagination="pagination" class="emojis" ref="emojis">
|
||||
|
@ -33,7 +33,7 @@
|
|||
<mk-button inline primary @click="add"><fa :icon="faPlus"/> {{ $t('addEmoji') }}</mk-button>
|
||||
</div>
|
||||
</section>
|
||||
<section class="_card remote">
|
||||
<section class="_card _vMargin remote">
|
||||
<div class="_title"><fa :icon="faLaugh"/> {{ $t('customEmojisOfRemote') }}</div>
|
||||
<div class="_content">
|
||||
<mk-input v-model="host" :debounce="true"><span>{{ $t('host') }}</span></mk-input>
|
||||
|
|
|
@ -66,7 +66,7 @@ import MkButton from '../../components/ui/button.vue';
|
|||
import MkInput from '../../components/ui/input.vue';
|
||||
import MkSelect from '../../components/ui/select.vue';
|
||||
import MkPagination from '../../components/ui/pagination.vue';
|
||||
import MkInstanceInfo from './federation.instance.vue';
|
||||
import MkInstanceInfo from './instance.vue';
|
||||
|
||||
export default defineComponent({
|
||||
metaInfo() {
|
||||
|
|
|
@ -0,0 +1,198 @@
|
|||
<template>
|
||||
<mk-container :body-togglable="false">
|
||||
<template #header><slot name="title"></slot></template>
|
||||
<template #func><button class="_button" @click="resume" :disabled="!paused"><fa :icon="faPlay"/></button><button class="_button" @click="pause" :disabled="paused"><fa :icon="faPause"/></button></template>
|
||||
|
||||
<div class="_content _table">
|
||||
<div class="_row">
|
||||
<div class="_cell"><div class="_label">Process</div>{{ activeSincePrevTick | number }}</div>
|
||||
<div class="_cell"><div class="_label">Active</div>{{ active | number }}</div>
|
||||
<div class="_cell"><div class="_label">Waiting</div>{{ waiting | number }}</div>
|
||||
<div class="_cell"><div class="_label">Delayed</div>{{ delayed | number }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="_content" style="margin-bottom: -8px;">
|
||||
<canvas ref="chart"></canvas>
|
||||
</div>
|
||||
</mk-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import Chart from 'chart.js';
|
||||
import { faPlay, faPause } from '@fortawesome/free-solid-svg-icons';
|
||||
import MkContainer from '../../components/ui/container.vue';
|
||||
|
||||
const alpha = (hex, a) => {
|
||||
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)!;
|
||||
const r = parseInt(result[1], 16);
|
||||
const g = parseInt(result[2], 16);
|
||||
const b = parseInt(result[3], 16);
|
||||
return `rgba(${r}, ${g}, ${b}, ${a})`;
|
||||
};
|
||||
|
||||
export default Vue.extend({
|
||||
components: {
|
||||
MkContainer,
|
||||
},
|
||||
|
||||
props: {
|
||||
domain: {
|
||||
required: true
|
||||
},
|
||||
connection: {
|
||||
required: true
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
chart: null,
|
||||
activeSincePrevTick: 0,
|
||||
active: 0,
|
||||
waiting: 0,
|
||||
delayed: 0,
|
||||
paused: false,
|
||||
faPlay, faPause
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
// TODO: var(--panel)の色が暗いか明るいかで判定する
|
||||
const gridColor = this.$store.state.device.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
|
||||
|
||||
Chart.defaults.global.defaultFontColor = getComputedStyle(document.documentElement).getPropertyValue('--fg');
|
||||
|
||||
this.chart = new Chart(this.$refs.chart, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: [],
|
||||
datasets: [{
|
||||
label: 'Process',
|
||||
pointRadius: 0,
|
||||
lineTension: 0,
|
||||
borderWidth: 0,
|
||||
backgroundColor: '#8BC34A',
|
||||
data: []
|
||||
}, {
|
||||
label: 'Active',
|
||||
pointRadius: 0,
|
||||
lineTension: 0,
|
||||
borderWidth: 0,
|
||||
backgroundColor: '#03A9F4',
|
||||
data: []
|
||||
}, {
|
||||
label: 'Waiting',
|
||||
pointRadius: 0,
|
||||
lineTension: 0,
|
||||
borderWidth: 0,
|
||||
backgroundColor: '#FFC107',
|
||||
data: []
|
||||
}, {
|
||||
label: 'Delayed',
|
||||
order: -1,
|
||||
type: 'line',
|
||||
pointRadius: 0,
|
||||
lineTension: 0,
|
||||
borderWidth: 2,
|
||||
borderColor: '#F44336',
|
||||
borderDash: [5, 5],
|
||||
fill: false,
|
||||
data: []
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
aspectRatio: 3,
|
||||
layout: {
|
||||
padding: {
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 8,
|
||||
bottom: 0
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
position: 'bottom',
|
||||
labels: {
|
||||
boxWidth: 16,
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
xAxes: [{
|
||||
stacked: true,
|
||||
gridLines: {
|
||||
display: false,
|
||||
color: gridColor,
|
||||
zeroLineColor: gridColor,
|
||||
},
|
||||
ticks: {
|
||||
display: false
|
||||
}
|
||||
}],
|
||||
yAxes: [{
|
||||
stacked: true,
|
||||
position: 'right',
|
||||
gridLines: {
|
||||
display: true,
|
||||
color: gridColor,
|
||||
zeroLineColor: gridColor,
|
||||
},
|
||||
ticks: {
|
||||
display: false,
|
||||
}
|
||||
}]
|
||||
},
|
||||
tooltips: {
|
||||
intersect: false,
|
||||
mode: 'index',
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.connection.on('stats', this.onStats);
|
||||
this.connection.on('statsLog', this.onStatsLog);
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
this.connection.off('stats', this.onStats);
|
||||
this.connection.off('statsLog', this.onStatsLog);
|
||||
},
|
||||
|
||||
methods: {
|
||||
onStats(stats) {
|
||||
if (this.paused) return;
|
||||
this.activeSincePrevTick = stats[this.domain].activeSincePrevTick;
|
||||
this.active = stats[this.domain].active;
|
||||
this.waiting = stats[this.domain].waiting;
|
||||
this.delayed = stats[this.domain].delayed;
|
||||
this.chart.data.labels.push('');
|
||||
this.chart.data.datasets[0].data.push(stats[this.domain].activeSincePrevTick);
|
||||
this.chart.data.datasets[1].data.push(stats[this.domain].active);
|
||||
this.chart.data.datasets[2].data.push(stats[this.domain].waiting);
|
||||
this.chart.data.datasets[3].data.push(stats[this.domain].delayed);
|
||||
if (this.chart.data.datasets[0].data.length > 100) {
|
||||
this.chart.data.labels.shift();
|
||||
this.chart.data.datasets[0].data.shift();
|
||||
this.chart.data.datasets[1].data.shift();
|
||||
this.chart.data.datasets[2].data.shift();
|
||||
this.chart.data.datasets[3].data.shift();
|
||||
}
|
||||
this.chart.update();
|
||||
},
|
||||
|
||||
onStatsLog(statsLog) {
|
||||
for (const stats of [...statsLog].reverse()) {
|
||||
this.onStats(stats);
|
||||
}
|
||||
},
|
||||
|
||||
pause() {
|
||||
this.paused = true;
|
||||
},
|
||||
|
||||
resume() {
|
||||
this.paused = false;
|
||||
},
|
||||
}
|
||||
});
|
||||
</script>
|
|
@ -1,9 +1,151 @@
|
|||
<template>
|
||||
<div v-if="meta" class="xhexznfu">
|
||||
<div v-if="meta" class="xhexznfu" v-size="{ min: [1600] }">
|
||||
<portal to="icon"><fa :icon="faServer"/></portal>
|
||||
<portal to="title">{{ $t('instance') }}</portal>
|
||||
|
||||
<mk-instance-stats style="margin-bottom: var(--margin);"/>
|
||||
<mk-folder>
|
||||
<template #header><fa :icon="faTachometerAlt"/> {{ $t('overview') }}</template>
|
||||
|
||||
<div class="sboqnrfi" :style="{ gridTemplateRows: overviewHeight }">
|
||||
<mk-instance-stats :chart-limit="300" :detailed="true" class="stats" ref="stats"/>
|
||||
|
||||
<div class="column">
|
||||
<mk-container :body-togglable="true" :resize-base-el="() => $el" class="info">
|
||||
<template #header><fa :icon="faInfoCircle"/>{{ $t('instanceInfo') }}</template>
|
||||
|
||||
<div class="_content">
|
||||
<div class="_keyValue"><b>Misskey</b><span>v{{ version }}</span></div>
|
||||
</div>
|
||||
<div class="_content" v-if="serverInfo">
|
||||
<div class="_keyValue"><b>Node.js</b><span>{{ serverInfo.node }}</span></div>
|
||||
<div class="_keyValue"><b>PostgreSQL</b><span>v{{ serverInfo.psql }}</span></div>
|
||||
<div class="_keyValue"><b>Redis</b><span>v{{ serverInfo.redis }}</span></div>
|
||||
</div>
|
||||
</mk-container>
|
||||
|
||||
<mk-container :body-togglable="true" :scrollable="true" :resize-base-el="() => $el" class="db">
|
||||
<template #header><fa :icon="faDatabase"/>{{ $t('database') }}</template>
|
||||
|
||||
<div class="_content" v-if="dbInfo">
|
||||
<table style="border-collapse: collapse; width: 100%;">
|
||||
<tr style="opacity: 0.7;">
|
||||
<th style="text-align: left; padding: 0 8px 8px 0;">Table</th>
|
||||
<th style="text-align: left; padding: 0 8px 8px 0;">Records</th>
|
||||
<th style="text-align: left; padding: 0 0 8px 0;">Size</th>
|
||||
</tr>
|
||||
<tr v-for="table in dbInfo" :key="table[0]">
|
||||
<th style="text-align: left; padding: 0 8px 0 0; word-break: break-all;">{{ table[0] }}</th>
|
||||
<td style="padding: 0 8px 0 0;">{{ number(table[1].count) }}</td>
|
||||
<td style="padding: 0; opacity: 0.7;">{{ bytes(table[1].size) }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</mk-container>
|
||||
|
||||
<mkw-federation class="fed" :body-togglable="true" :scrollable="true"/>
|
||||
</div>
|
||||
</div>
|
||||
</mk-folder>
|
||||
|
||||
<mk-folder style="margin: var(--margin) 0;">
|
||||
<template #header><fa :icon="faHeartbeat"/> {{ $t('metrics') }}</template>
|
||||
|
||||
<div class="segusily">
|
||||
<mk-container :body-togglable="false" :resize-base-el="() => $el">
|
||||
<template #header><fa :icon="faMicrochip"/>{{ $t('cpuAndMemory') }}</template>
|
||||
<template #func><button class="_button" @click="resume" :disabled="!paused"><fa :icon="faPlay"/></button><button class="_button" @click="pause" :disabled="paused"><fa :icon="faPause"/></button></template>
|
||||
|
||||
<div class="_content" style="margin-top: -8px; margin-bottom: -12px;">
|
||||
<canvas ref="cpumem"></canvas>
|
||||
</div>
|
||||
<div class="_content" v-if="serverInfo">
|
||||
<div class="_table">
|
||||
<!--
|
||||
<div class="_row">
|
||||
<div class="_cell"><div class="_label">CPU</div>{{ serverInfo.cpu.model }}</div>
|
||||
</div>
|
||||
-->
|
||||
<div class="_row">
|
||||
<div class="_cell"><div class="_label">MEM total</div>{{ bytes(serverInfo.mem.total) }}</div>
|
||||
<div class="_cell"><div class="_label">MEM used</div>{{ bytes(memUsage) }} ({{ (memUsage / serverInfo.mem.total * 100).toFixed(0) }}%)</div>
|
||||
<div class="_cell"><div class="_label">MEM free</div>{{ bytes(serverInfo.mem.total - memUsage) }} ({{ ((serverInfo.mem.total - memUsage) / serverInfo.mem.total * 100).toFixed(0) }}%)</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</mk-container>
|
||||
|
||||
<mk-container :body-togglable="false" :resize-base-el="() => $el">
|
||||
<template #header><fa :icon="faHdd"/> {{ $t('disk') }}</template>
|
||||
<template #func><button class="_button" @click="resume" :disabled="!paused"><fa :icon="faPlay"/></button><button class="_button" @click="pause" :disabled="paused"><fa :icon="faPause"/></button></template>
|
||||
|
||||
<div class="_content" style="margin-top: -8px; margin-bottom: -12px;">
|
||||
<canvas ref="disk"></canvas>
|
||||
</div>
|
||||
<div class="_content" v-if="serverInfo">
|
||||
<div class="_table">
|
||||
<div class="_row">
|
||||
<div class="_cell"><div class="_label">Disk total</div>{{ bytes(serverInfo.fs.total) }}</div>
|
||||
<div class="_cell"><div class="_label">Disk used</div>{{ bytes(serverInfo.fs.used) }} ({{ (serverInfo.fs.used / serverInfo.fs.total * 100).toFixed(0) }}%)</div>
|
||||
<div class="_cell"><div class="_label">Disk free</div>{{ bytes(serverInfo.fs.total - serverInfo.fs.used) }} ({{ ((serverInfo.fs.total - serverInfo.fs.used) / serverInfo.fs.total * 100).toFixed(0) }}%)</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</mk-container>
|
||||
|
||||
<mk-container :body-togglable="false" :resize-base-el="() => $el">
|
||||
<template #header><fa :icon="faExchangeAlt"/> {{ $t('network') }}</template>
|
||||
<template #func><button class="_button" @click="resume" :disabled="!paused"><fa :icon="faPlay"/></button><button class="_button" @click="pause" :disabled="paused"><fa :icon="faPause"/></button></template>
|
||||
|
||||
<div class="_content" style="margin-top: -8px; margin-bottom: -12px;">
|
||||
<canvas ref="net"></canvas>
|
||||
</div>
|
||||
<div class="_content" v-if="serverInfo">
|
||||
<div class="_table">
|
||||
<div class="_row">
|
||||
<div class="_cell"><div class="_label">Interface</div>{{ serverInfo.net.interface }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</mk-container>
|
||||
</div>
|
||||
</mk-folder>
|
||||
|
||||
<mk-folder>
|
||||
<template #header><fa :icon="faClipboardList"/> {{ $t('jobQueue') }}</template>
|
||||
|
||||
<div class="vkyrmkwb" :style="{ gridTemplateRows: queueHeight }">
|
||||
<mk-container :body-togglable="false" :scrollable="true" :resize-base-el="() => $el">
|
||||
<template #header><fa :icon="faExclamationTriangle"/> {{ $t('delayed') }}</template>
|
||||
|
||||
<div class="_content">
|
||||
<div class="_keyValue" v-for="job in jobs" :key="job[0]">
|
||||
<button class="_button" @click="showInstanceInfo(job[0])">{{ job[0] }}</button>
|
||||
<div style="text-align: right;">{{ number(job[1]) }} jobs</div>
|
||||
</div>
|
||||
</div>
|
||||
</mk-container>
|
||||
<x-queue :connection="queueConnection" domain="inbox" ref="queue" class="queue">
|
||||
<template #title><fa :icon="faExchangeAlt"/> In</template>
|
||||
</x-queue>
|
||||
<x-queue :connection="queueConnection" domain="deliver" class="queue">
|
||||
<template #title><fa :icon="faExchangeAlt"/> Out</template>
|
||||
</x-queue>
|
||||
</div>
|
||||
</mk-folder>
|
||||
|
||||
<mk-folder>
|
||||
<template #header><fa :icon="faStream"/> {{ $t('logs') }}</template>
|
||||
|
||||
<div class="uwuemslx">
|
||||
<mk-container :body-togglable="false" :resize-base-el="() => $el">
|
||||
<template #header><fa :icon="faInfoCircle"/>{{ $t('') }}</template>
|
||||
|
||||
<div class="_content">
|
||||
<div class="_keyValue" v-for="log in modLogs">
|
||||
<b>{{ log.type }}</b><span>by {{ log.user.username }}</span><mk-time :time="log.createdAt" style="opacity: 0.7;"/>
|
||||
</div>
|
||||
</div>
|
||||
</mk-container>
|
||||
|
||||
<section class="_card logs">
|
||||
<div class="_title"><fa :icon="faStream"/> {{ $t('serverLogs') }}</div>
|
||||
|
@ -36,78 +178,27 @@
|
|||
<mk-button @click="deleteAllLogs()" primary><fa :icon="faTrashAlt"/> {{ $t('deleteAll') }}</mk-button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="_card chart">
|
||||
<div class="_title"><fa :icon="faMicrochip"/> {{ $t('cpuAndMemory') }}</div>
|
||||
<div class="_content" style="margin-top: -8px; margin-bottom: -12px;">
|
||||
<canvas ref="cpumem"></canvas>
|
||||
</div>
|
||||
<div class="_content" v-if="serverInfo">
|
||||
<div class="table">
|
||||
<div class="row">
|
||||
<div class="cell"><div class="label">CPU</div>{{ serverInfo.cpu.model }}</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="cell"><div class="label">MEM total</div>{{ bytes(serverInfo.mem.total) }}</div>
|
||||
<div class="cell"><div class="label">MEM used</div>{{ bytes(memUsage) }} ({{ (memUsage / serverInfo.mem.total * 100).toFixed(0) }}%)</div>
|
||||
<div class="cell"><div class="label">MEM free</div>{{ bytes(serverInfo.mem.total - memUsage) }} ({{ ((serverInfo.mem.total - memUsage) / serverInfo.mem.total * 100).toFixed(0) }}%)</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="_card chart">
|
||||
<div class="_title"><fa :icon="faHdd"/> {{ $t('disk') }}</div>
|
||||
<div class="_content" style="margin-top: -8px; margin-bottom: -12px;">
|
||||
<canvas ref="disk"></canvas>
|
||||
</div>
|
||||
<div class="_content" v-if="serverInfo">
|
||||
<div class="table">
|
||||
<div class="row">
|
||||
<div class="cell"><div class="label">Disk total</div>{{ bytes(serverInfo.fs.total) }}</div>
|
||||
<div class="cell"><div class="label">Disk used</div>{{ bytes(serverInfo.fs.used) }} ({{ (serverInfo.fs.used / serverInfo.fs.total * 100).toFixed(0) }}%)</div>
|
||||
<div class="cell"><div class="label">Disk free</div>{{ bytes(serverInfo.fs.total - serverInfo.fs.used) }} ({{ ((serverInfo.fs.total - serverInfo.fs.used) / serverInfo.fs.total * 100).toFixed(0) }}%)</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="_card chart">
|
||||
<div class="_title"><fa :icon="faExchangeAlt"/> {{ $t('network') }}</div>
|
||||
<div class="_content" style="margin-top: -8px; margin-bottom: -12px;">
|
||||
<canvas ref="net"></canvas>
|
||||
</div>
|
||||
<div class="_content" v-if="serverInfo">
|
||||
<div class="table">
|
||||
<div class="row">
|
||||
<div class="cell"><div class="label">Interface</div>{{ serverInfo.net.interface }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="_card info">
|
||||
<div class="_content table">
|
||||
<div><b>Misskey</b><span>v{{ version }}</span></div>
|
||||
</div>
|
||||
<div class="_content table" v-if="serverInfo">
|
||||
<div><b>Node.js</b><span>{{ serverInfo.node }}</span></div>
|
||||
<div><b>PostgreSQL</b><span>v{{ serverInfo.psql }}</span></div>
|
||||
<div><b>Redis</b><span>v{{ serverInfo.redis }}</span></div>
|
||||
</div>
|
||||
</section>
|
||||
</mk-folder>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import { faServer, faExchangeAlt, faMicrochip, faHdd, faStream, faTrashAlt } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faPlay, faPause, faDatabase, faServer, faExchangeAlt, faMicrochip, faHdd, faStream, faTrashAlt, faInfoCircle, faExclamationTriangle, faTachometerAlt, faHeartbeat, faClipboardList } from '@fortawesome/free-solid-svg-icons';
|
||||
import Chart from 'chart.js';
|
||||
import VueJsonPretty from 'vue-json-pretty';
|
||||
import MkInstanceStats from '../../components/instance-stats.vue';
|
||||
import MkButton from '../../components/ui/button.vue';
|
||||
import MkSelect from '../../components/ui/select.vue';
|
||||
import MkInput from '../../components/ui/input.vue';
|
||||
import MkContainer from '../../components/ui/container.vue';
|
||||
import MkFolder from '../../components/ui/folder.vue';
|
||||
import MkwFederation from '../../widgets/federation.vue';
|
||||
import { version, url } from '../../config';
|
||||
import bytes from '../../filters/bytes';
|
||||
import XQueue from './index.queue-chart.vue';
|
||||
import MkInstanceInfo from './instance.vue';
|
||||
|
||||
const alpha = (hex, a) => {
|
||||
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)!;
|
||||
|
@ -129,7 +220,11 @@ export default defineComponent({
|
|||
MkButton,
|
||||
MkSelect,
|
||||
MkInput,
|
||||
VueJsonPretty
|
||||
MkContainer,
|
||||
MkFolder,
|
||||
MkwFederation,
|
||||
XQueue,
|
||||
VueJsonPretty,
|
||||
},
|
||||
|
||||
data() {
|
||||
|
@ -139,13 +234,20 @@ export default defineComponent({
|
|||
stats: null,
|
||||
serverInfo: null,
|
||||
connection: null,
|
||||
queueConnection: this.$root.stream.useSharedConnection('queueStats'),
|
||||
memUsage: 0,
|
||||
chartCpuMem: null,
|
||||
chartNet: null,
|
||||
jobs: [],
|
||||
logs: [],
|
||||
logLevel: 'all',
|
||||
logDomain: '',
|
||||
faServer, faExchangeAlt, faMicrochip, faHdd, faStream, faTrashAlt
|
||||
modLogs: [],
|
||||
dbInfo: null,
|
||||
overviewHeight: '1fr',
|
||||
queueHeight: '1fr',
|
||||
paused: false,
|
||||
faPlay, faPause, faDatabase, faServer, faExchangeAlt, faMicrochip, faHdd, faStream, faTrashAlt, faInfoCircle, faExclamationTriangle, faTachometerAlt, faHeartbeat, faClipboardList,
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -166,8 +268,17 @@ export default defineComponent({
|
|||
}
|
||||
},
|
||||
|
||||
created() {
|
||||
this.$store.commit('setFullView', true);
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.fetchLogs();
|
||||
this.fetchJobs();
|
||||
this.fetchModLogs();
|
||||
|
||||
// TODO: var(--panel)の色が暗いか明るいかで判定する
|
||||
const gridColor = this.$store.state.device.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
|
||||
|
||||
Chart.defaults.global.defaultFontColor = getComputedStyle(document.documentElement).getPropertyValue('--fg');
|
||||
|
||||
|
@ -221,14 +332,21 @@ export default defineComponent({
|
|||
scales: {
|
||||
xAxes: [{
|
||||
gridLines: {
|
||||
display: false
|
||||
display: false,
|
||||
color: gridColor,
|
||||
zeroLineColor: gridColor,
|
||||
},
|
||||
ticks: {
|
||||
display: false
|
||||
display: false,
|
||||
}
|
||||
}],
|
||||
yAxes: [{
|
||||
position: 'right',
|
||||
gridLines: {
|
||||
display: true,
|
||||
color: gridColor,
|
||||
zeroLineColor: gridColor,
|
||||
},
|
||||
ticks: {
|
||||
display: false,
|
||||
max: 100
|
||||
|
@ -283,7 +401,9 @@ export default defineComponent({
|
|||
scales: {
|
||||
xAxes: [{
|
||||
gridLines: {
|
||||
display: false
|
||||
display: false,
|
||||
color: gridColor,
|
||||
zeroLineColor: gridColor,
|
||||
},
|
||||
ticks: {
|
||||
display: false
|
||||
|
@ -291,6 +411,11 @@ export default defineComponent({
|
|||
}],
|
||||
yAxes: [{
|
||||
position: 'right',
|
||||
gridLines: {
|
||||
display: true,
|
||||
color: gridColor,
|
||||
zeroLineColor: gridColor,
|
||||
},
|
||||
ticks: {
|
||||
display: false,
|
||||
}
|
||||
|
@ -344,7 +469,9 @@ export default defineComponent({
|
|||
scales: {
|
||||
xAxes: [{
|
||||
gridLines: {
|
||||
display: false
|
||||
display: false,
|
||||
color: gridColor,
|
||||
zeroLineColor: gridColor,
|
||||
},
|
||||
ticks: {
|
||||
display: false
|
||||
|
@ -352,6 +479,11 @@ export default defineComponent({
|
|||
}],
|
||||
yAxes: [{
|
||||
position: 'right',
|
||||
gridLines: {
|
||||
display: true,
|
||||
color: gridColor,
|
||||
zeroLineColor: gridColor,
|
||||
},
|
||||
ticks: {
|
||||
display: false,
|
||||
}
|
||||
|
@ -374,6 +506,31 @@ export default defineComponent({
|
|||
id: Math.random().toString().substr(2, 8),
|
||||
length: 150
|
||||
});
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.queueConnection.send('requestLog', {
|
||||
id: Math.random().toString().substr(2, 8),
|
||||
length: 200
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
this.$root.api('admin/get-table-stats', {}).then(res => {
|
||||
this.dbInfo = Object.entries(res).sort((a, b) => b[1].size - a[1].size);
|
||||
});
|
||||
|
||||
this.$nextTick(() => {
|
||||
new ResizeObserver((entries, observer) => {
|
||||
if (this.$refs.stats && this.$refs.stats.$el) {
|
||||
this.overviewHeight = this.$refs.stats.$el.offsetHeight + 'px';
|
||||
}
|
||||
}).observe(this.$refs.stats.$el);
|
||||
|
||||
new ResizeObserver((entries, observer) => {
|
||||
if (this.$refs.queue && this.$refs.queue.$el) {
|
||||
this.queueHeight = this.$refs.queue.$el.offsetHeight + 'px';
|
||||
}
|
||||
}).observe(this.$refs.queue.$el);
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -381,9 +538,23 @@ export default defineComponent({
|
|||
this.connection.off('stats', this.onStats);
|
||||
this.connection.off('statsLog', this.onStatsLog);
|
||||
this.connection.dispose();
|
||||
this.queueConnection.dispose();
|
||||
this.$store.commit('setFullView', false);
|
||||
},
|
||||
|
||||
methods: {
|
||||
async showInstanceInfo(q) {
|
||||
let instance = q;
|
||||
if (typeof q === 'string') {
|
||||
instance = await this.$root.api('federation/show-instance', {
|
||||
host: q
|
||||
});
|
||||
}
|
||||
this.$root.new(MkInstanceInfo, {
|
||||
instance: instance
|
||||
});
|
||||
},
|
||||
|
||||
fetchLogs() {
|
||||
this.$root.api('admin/logs', {
|
||||
level: this.logLevel === 'all' ? null : this.logLevel,
|
||||
|
@ -394,6 +565,18 @@ export default defineComponent({
|
|||
});
|
||||
},
|
||||
|
||||
fetchJobs() {
|
||||
this.$root.api('admin/queue/deliver-delayed', {}).then(jobs => {
|
||||
this.jobs = jobs;
|
||||
});
|
||||
},
|
||||
|
||||
fetchModLogs() {
|
||||
this.$root.api('admin/show-moderation-logs', {}).then(logs => {
|
||||
this.modLogs = logs;
|
||||
});
|
||||
},
|
||||
|
||||
deleteAllLogs() {
|
||||
this.$root.api('admin/delete-logs').then(() => {
|
||||
this.$root.dialog({
|
||||
|
@ -404,6 +587,8 @@ export default defineComponent({
|
|||
},
|
||||
|
||||
onStats(stats) {
|
||||
if (this.paused) return;
|
||||
|
||||
const cpu = (stats.cpu * 100).toFixed(0);
|
||||
const memActive = (stats.mem.active / this.serverInfo.mem.total * 100).toFixed(0);
|
||||
const memUsed = (stats.mem.used / this.serverInfo.mem.total * 100).toFixed(0);
|
||||
|
@ -442,13 +627,97 @@ export default defineComponent({
|
|||
}
|
||||
},
|
||||
|
||||
bytes
|
||||
bytes,
|
||||
|
||||
pause() {
|
||||
this.paused = true;
|
||||
},
|
||||
|
||||
resume() {
|
||||
this.paused = false;
|
||||
},
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.xhexznfu {
|
||||
&.min-width_1600px {
|
||||
.sboqnrfi {
|
||||
display: grid;
|
||||
grid-template-columns: 3.2fr 1fr;
|
||||
grid-template-rows: 1fr;
|
||||
gap: 16px 16px;
|
||||
|
||||
> .stats {
|
||||
height: min-content;
|
||||
}
|
||||
|
||||
> .column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
> .info {
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
||||
> .db {
|
||||
flex: 1;
|
||||
flex-grow: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
> .fed {
|
||||
flex: 1;
|
||||
flex-grow: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
> *:not(:last-child) {
|
||||
margin-bottom: var(--margin);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.segusily {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
grid-template-rows: 1fr;
|
||||
gap: 16px 16px;
|
||||
}
|
||||
|
||||
.vkyrmkwb {
|
||||
display: grid;
|
||||
grid-template-columns: 0.5fr 1fr 1fr;
|
||||
grid-template-rows: 1fr;
|
||||
gap: 16px 16px;
|
||||
margin-bottom: var(--margin);
|
||||
|
||||
> .queue {
|
||||
height: min-content;
|
||||
}
|
||||
|
||||
> * {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.uwuemslx {
|
||||
display: grid;
|
||||
grid-template-columns: 2fr 3fr;
|
||||
grid-template-rows: 1fr;
|
||||
gap: 16px 16px;
|
||||
height: 400px;
|
||||
}
|
||||
}
|
||||
|
||||
.vkyrmkwb {
|
||||
> * {
|
||||
margin-bottom: var(--margin);
|
||||
}
|
||||
}
|
||||
|
||||
> .stats {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
@ -494,49 +763,5 @@ export default defineComponent({
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .chart {
|
||||
> ._content {
|
||||
> .table {
|
||||
> .row {
|
||||
display: flex;
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-bottom: 16px;
|
||||
|
||||
@media (max-width: 500px) {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
> .cell {
|
||||
flex: 1;
|
||||
|
||||
> .label {
|
||||
font-size: 80%;
|
||||
opacity: 0.7;
|
||||
|
||||
> .icon {
|
||||
margin-right: 4px;
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .info {
|
||||
> .table {
|
||||
> div {
|
||||
display: flex;
|
||||
|
||||
> * {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -2,69 +2,69 @@
|
|||
<x-window @closed="() => { $emit('closed'); destroyDom(); }" :no-padding="true" :width="520" :height="500">
|
||||
<template #header>{{ instance.host }}</template>
|
||||
<div class="mk-instance-info">
|
||||
<div class="table info">
|
||||
<div class="row">
|
||||
<div class="cell">
|
||||
<div class="label">{{ $t('software') }}</div>
|
||||
<div class="data">{{ instance.softwareName || '?' }}</div>
|
||||
<div class="_table">
|
||||
<div class="_row">
|
||||
<div class="_cell">
|
||||
<div class="_label">{{ $t('software') }}</div>
|
||||
<div class="_data">{{ instance.softwareName || '?' }}</div>
|
||||
</div>
|
||||
<div class="cell">
|
||||
<div class="label">{{ $t('version') }}</div>
|
||||
<div class="data">{{ instance.softwareVersion || '?' }}</div>
|
||||
<div class="_cell">
|
||||
<div class="_label">{{ $t('version') }}</div>
|
||||
<div class="_data">{{ instance.softwareVersion || '?' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table data">
|
||||
<div class="row">
|
||||
<div class="cell">
|
||||
<div class="label"><fa :icon="faCrosshairs" fixed-width class="icon"/>{{ $t('registeredAt') }}</div>
|
||||
<div class="data">{{ new Date(instance.caughtAt).toLocaleString() }} (<mk-time :time="instance.caughtAt"/>)</div>
|
||||
<div class="_table data">
|
||||
<div class="_row">
|
||||
<div class="_cell">
|
||||
<div class="_label">{{ $t('registeredAt') }}</div>
|
||||
<div class="_data">{{ new Date(instance.caughtAt).toLocaleString() }} (<mk-time :time="instance.caughtAt"/>)</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="cell">
|
||||
<div class="label"><fa :icon="faCloudDownloadAlt" fixed-width class="icon"/>{{ $t('following') }}</div>
|
||||
<div class="data clickable" @click="showFollowing()">{{ number(instance.followingCount) }}</div>
|
||||
<div class="_row">
|
||||
<div class="_cell">
|
||||
<div class="_label">{{ $t('following') }}</div>
|
||||
<button class="_data _textButton" @click="showFollowing()">{{ number(instance.followingCount) }}</button>
|
||||
</div>
|
||||
<div class="cell">
|
||||
<div class="label"><fa :icon="faCloudUploadAlt" fixed-width class="icon"/>{{ $t('followers') }}</div>
|
||||
<div class="data clickable" @click="showFollowers()">{{ number(instance.followersCount) }}</div>
|
||||
<div class="_cell">
|
||||
<div class="_label">{{ $t('followers') }}</div>
|
||||
<button class="_data _textButton" @click="showFollowers()">{{ number(instance.followersCount) }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="cell">
|
||||
<div class="label"><fa :icon="faUsers" fixed-width class="icon"/>{{ $t('users') }}</div>
|
||||
<div class="data clickable" @click="showUsers()">{{ number(instance.usersCount) }}</div>
|
||||
<div class="_row">
|
||||
<div class="_cell">
|
||||
<div class="_label">{{ $t('users') }}</div>
|
||||
<button class="_data _textButton" @click="showUsers()">{{ number(instance.usersCount) }}</button>
|
||||
</div>
|
||||
<div class="cell">
|
||||
<div class="label"><fa :icon="faPencilAlt" fixed-width class="icon"/>{{ $t('notes') }}</div>
|
||||
<div class="data">{{ number(instance.notesCount) }}</div>
|
||||
<div class="_cell">
|
||||
<div class="_label">{{ $t('notes') }}</div>
|
||||
<div class="_data">{{ number(instance.notesCount) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="cell">
|
||||
<div class="label"><fa :icon="faFileImage" fixed-width class="icon"/>{{ $t('files') }}</div>
|
||||
<div class="data">{{ number(instance.driveFiles) }}</div>
|
||||
<div class="_row">
|
||||
<div class="_cell">
|
||||
<div class="_label">{{ $t('files') }}</div>
|
||||
<div class="_data">{{ number(instance.driveFiles) }}</div>
|
||||
</div>
|
||||
<div class="cell">
|
||||
<div class="label"><fa :icon="faDatabase" fixed-width class="icon"/>{{ $t('storageUsage') }}</div>
|
||||
<div class="data">{{ bytes(instance.driveUsage) }}</div>
|
||||
<div class="_cell">
|
||||
<div class="_label">{{ $t('storageUsage') }}</div>
|
||||
<div class="_data">{{ bytes(instance.driveUsage) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="cell">
|
||||
<div class="label"><fa :icon="faLongArrowAltUp" fixed-width class="icon"/>{{ $t('latestRequestSentAt') }}</div>
|
||||
<div class="data"><mk-time v-if="instance.latestRequestSentAt" :time="instance.latestRequestSentAt"/><span v-else>N/A</span></div>
|
||||
<div class="_row">
|
||||
<div class="_cell">
|
||||
<div class="_label">{{ $t('latestRequestSentAt') }}</div>
|
||||
<div class="_data"><mk-time v-if="instance.latestRequestSentAt" :time="instance.latestRequestSentAt"/><span v-else>N/A</span></div>
|
||||
</div>
|
||||
<div class="cell">
|
||||
<div class="label"><fa :icon="faTrafficLight" fixed-width class="icon"/>{{ $t('latestStatus') }}</div>
|
||||
<div class="data">{{ instance.latestStatus ? instance.latestStatus : 'N/A' }}</div>
|
||||
<div class="_cell">
|
||||
<div class="_label">{{ $t('latestStatus') }}</div>
|
||||
<div class="_data">{{ instance.latestStatus ? instance.latestStatus : 'N/A' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="cell">
|
||||
<div class="label"><fa :icon="faLongArrowAltDown" fixed-width class="icon"/>{{ $t('latestRequestReceivedAt') }}</div>
|
||||
<div class="data"><mk-time v-if="instance.latestRequestReceivedAt" :time="instance.latestRequestReceivedAt"/><span v-else>N/A</span></div>
|
||||
<div class="_row">
|
||||
<div class="_cell">
|
||||
<div class="_label">{{ $t('latestRequestReceivedAt') }}</div>
|
||||
<div class="_data"><mk-time v-if="instance.latestRequestReceivedAt" :time="instance.latestRequestReceivedAt"/><span v-else>N/A</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -489,39 +489,12 @@ export default defineComponent({
|
|||
.mk-instance-info {
|
||||
overflow: auto;
|
||||
|
||||
> .table {
|
||||
> ._table {
|
||||
padding: 0 32px;
|
||||
|
||||
@media (max-width: 500px) {
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
> .row {
|
||||
display: flex;
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
> .cell {
|
||||
flex: 1;
|
||||
|
||||
> .label {
|
||||
font-size: 80%;
|
||||
opacity: 0.7;
|
||||
|
||||
> .icon {
|
||||
margin-right: 4px;
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
> .data.clickable {
|
||||
color: var(--accent);
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .data {
|
|
@ -1,11 +1,13 @@
|
|||
<template>
|
||||
<section class="_card mk-queue-queue">
|
||||
<section class="_card">
|
||||
<div class="_title"><slot name="title"></slot></div>
|
||||
<div class="_content status">
|
||||
<div class="cell"><div class="label">Process</div>{{ number(activeSincePrevTick) }}</div>
|
||||
<div class="cell"><div class="label">Active</div>{{ number(active) }}</div>
|
||||
<div class="cell"><div class="label">Waiting</div>{{ number(waiting) }}</div>
|
||||
<div class="cell"><div class="label">Delayed</div>{{ number(delayed) }}</div>
|
||||
<div class="_content _table">
|
||||
<div class="_row">
|
||||
<div class="_cell"><div class="_label">Process</div>{{ number(activeSincePrevTick) }}</div>
|
||||
<div class="_cell"><div class="_label">Active</div>{{ number(active) }}</div>
|
||||
<div class="_cell"><div class="_label">Waiting</div>{{ number(waiting) }}</div>
|
||||
<div class="_cell"><div class="_label">Delayed</div>{{ number(delayed) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="_content" style="margin-bottom: -8px;">
|
||||
<canvas ref="chart"></canvas>
|
||||
|
@ -59,6 +61,9 @@ export default defineComponent({
|
|||
mounted() {
|
||||
this.fetchJobs();
|
||||
|
||||
// TODO: var(--panel)の色が暗いか明るいかで判定する
|
||||
const gridColor = this.$store.state.device.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
|
||||
|
||||
Chart.defaults.global.defaultFontColor = getComputedStyle(document.documentElement).getPropertyValue('--fg');
|
||||
|
||||
this.chart = new Chart(this.$refs.chart, {
|
||||
|
@ -119,7 +124,9 @@ export default defineComponent({
|
|||
scales: {
|
||||
xAxes: [{
|
||||
gridLines: {
|
||||
display: false
|
||||
display: false,
|
||||
color: gridColor,
|
||||
zeroLineColor: gridColor,
|
||||
},
|
||||
ticks: {
|
||||
display: false
|
||||
|
@ -127,6 +134,11 @@ export default defineComponent({
|
|||
}],
|
||||
yAxes: [{
|
||||
position: 'right',
|
||||
gridLines: {
|
||||
display: true,
|
||||
color: gridColor,
|
||||
zeroLineColor: gridColor,
|
||||
},
|
||||
ticks: {
|
||||
display: false,
|
||||
}
|
||||
|
@ -185,20 +197,3 @@ export default defineComponent({
|
|||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.mk-queue-queue {
|
||||
> .status {
|
||||
display: flex;
|
||||
|
||||
> .cell {
|
||||
flex: 1;
|
||||
|
||||
> .label {
|
||||
font-size: 80%;
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -22,7 +22,7 @@ import { defineComponent } from 'vue';
|
|||
import { faExchangeAlt } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faTrashAlt } from '@fortawesome/free-regular-svg-icons';
|
||||
import MkButton from '../../components/ui/button.vue';
|
||||
import XQueue from './queue.queue.vue';
|
||||
import XQueue from './queue.chart.vue';
|
||||
|
||||
export default defineComponent({
|
||||
metaInfo() {
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<portal to="icon"><fa :icon="faProjectDiagram"/></portal>
|
||||
<portal to="title">{{ $t('relays') }}</portal>
|
||||
|
||||
<section class="_card add">
|
||||
<section class="_card _vMargin add">
|
||||
<div class="_title"><fa :icon="faPlus"/> {{ $t('addRelay') }}</div>
|
||||
<div class="_content">
|
||||
<mk-input v-model="inbox">
|
||||
|
@ -13,7 +13,7 @@
|
|||
</div>
|
||||
</section>
|
||||
|
||||
<section class="_card relays">
|
||||
<section class="_card _vMargin relays">
|
||||
<div class="_title"><fa :icon="faProjectDiagram"/> {{ $t('addedRelays') }}</div>
|
||||
<div class="_content relay" v-for="relay in relays" :key="relay.inbox">
|
||||
<div>{{ relay.inbox }}</div>
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<portal to="icon"><fa :icon="faCog"/></portal>
|
||||
<portal to="title">{{ $t('settings') }}</portal>
|
||||
|
||||
<section class="_card info">
|
||||
<section class="_card _vMargin info">
|
||||
<div class="_title"><fa :icon="faInfoCircle"/> {{ $t('basicInfo') }}</div>
|
||||
<div class="_content">
|
||||
<mk-input v-model="name">{{ $t('instanceName') }}</mk-input>
|
||||
|
@ -19,7 +19,7 @@
|
|||
</div>
|
||||
</section>
|
||||
|
||||
<section class="_card info">
|
||||
<section class="_card _vMargin info">
|
||||
<div class="_content">
|
||||
<mk-input v-model="maxNoteTextLength" type="number" :save="() => save()" style="margin:0;"><template #icon><fa :icon="faPencilAlt"/></template>{{ $t('maxNoteTextLength') }}</mk-input>
|
||||
</div>
|
||||
|
@ -33,7 +33,7 @@
|
|||
</div>
|
||||
</section>
|
||||
|
||||
<section class="_card info">
|
||||
<section class="_card _vMargin info">
|
||||
<div class="_title"><fa :icon="faUser"/> {{ $t('registration') }}</div>
|
||||
<div class="_content">
|
||||
<mk-switch v-model="enableRegistration" @change="save()">{{ $t('enableRegistration') }}</mk-switch>
|
||||
|
@ -41,7 +41,7 @@
|
|||
</div>
|
||||
</section>
|
||||
|
||||
<section class="_card">
|
||||
<section class="_card _vMargin">
|
||||
<div class="_title"><fa :icon="faShieldAlt"/> {{ $t('hcaptcha') }}</div>
|
||||
<div class="_content">
|
||||
<mk-switch v-model="enableHcaptcha" ref="enableHcaptcha">{{ $t('enableHcaptcha') }}</mk-switch>
|
||||
|
@ -59,7 +59,7 @@
|
|||
</div>
|
||||
</section>
|
||||
|
||||
<section class="_card">
|
||||
<section class="_card _vMargin">
|
||||
<div class="_title"><fa :icon="faShieldAlt"/> {{ $t('recaptcha') }}</div>
|
||||
<div class="_content">
|
||||
<mk-switch v-model="enableRecaptcha" ref="enableRecaptcha">{{ $t('enableRecaptcha') }}</mk-switch>
|
||||
|
@ -77,7 +77,7 @@
|
|||
</div>
|
||||
</section>
|
||||
|
||||
<section class="_card">
|
||||
<section class="_card _vMargin">
|
||||
<div class="_title"><fa :icon="faEnvelope" /> {{ $t('emailConfig') }}</div>
|
||||
<div class="_content">
|
||||
<mk-switch v-model="enableEmail" @change="save()">{{ $t('enableEmail') }}<template #desc>{{ $t('emailConfigInfo') }}</template></mk-switch>
|
||||
|
@ -100,7 +100,7 @@
|
|||
</div>
|
||||
</section>
|
||||
|
||||
<section class="_card">
|
||||
<section class="_card _vMargin">
|
||||
<div class="_title"><fa :icon="faBolt"/> {{ $t('serviceworker') }}</div>
|
||||
<div class="_content">
|
||||
<mk-switch v-model="enableServiceWorker">{{ $t('enableServiceworker') }}<template #desc>{{ $t('serviceworkerInfo') }}</template></mk-switch>
|
||||
|
@ -116,7 +116,7 @@
|
|||
</div>
|
||||
</section>
|
||||
|
||||
<section class="_card">
|
||||
<section class="_card _vMargin">
|
||||
<div class="_title"><fa :icon="faThumbtack"/> {{ $t('pinnedUsers') }}</div>
|
||||
<div class="_content">
|
||||
<mk-textarea v-model="pinnedUsers">
|
||||
|
@ -128,7 +128,7 @@
|
|||
</div>
|
||||
</section>
|
||||
|
||||
<section class="_card">
|
||||
<section class="_card _vMargin">
|
||||
<div class="_title"><fa :icon="faCloud"/> {{ $t('files') }}</div>
|
||||
<div class="_content">
|
||||
<mk-switch v-model="cacheRemoteFiles">{{ $t('cacheRemoteFiles') }}<template #desc>{{ $t('cacheRemoteFilesDescription') }}</template></mk-switch>
|
||||
|
@ -141,7 +141,7 @@
|
|||
</div>
|
||||
</section>
|
||||
|
||||
<section class="_card">
|
||||
<section class="_card _vMargin">
|
||||
<div class="_title"><fa :icon="faCloud"/> {{ $t('objectStorage') }}</div>
|
||||
<div class="_content">
|
||||
<mk-switch v-model="useObjectStorage">{{ $t('useObjectStorage') }}</mk-switch>
|
||||
|
@ -161,6 +161,7 @@
|
|||
</div>
|
||||
<mk-switch v-model="objectStorageUseSSL" :disabled="!useObjectStorage">{{ $t('objectStorageUseSSL') }}<template #desc>{{ $t('objectStorageUseSSLDesc') }}</template></mk-switch>
|
||||
<mk-switch v-model="objectStorageUseProxy" :disabled="!useObjectStorage">{{ $t('objectStorageUseProxy') }}<template #desc>{{ $t('objectStorageUseProxyDesc') }}</template></mk-switch>
|
||||
<mk-switch v-model="objectStorageSetPublicRead" :disabled="!useObjectStorage">{{ $t('objectStorageSetPublicRead') }}</mk-switch>
|
||||
</template>
|
||||
</div>
|
||||
<div class="_footer">
|
||||
|
@ -168,7 +169,7 @@
|
|||
</div>
|
||||
</section>
|
||||
|
||||
<section class="_card">
|
||||
<section class="_card _vMargin">
|
||||
<div class="_title"><fa :icon="faGhost"/> {{ $t('proxyAccount') }}</div>
|
||||
<div class="_content">
|
||||
<mk-input :value="proxyAccount ? proxyAccount.username : null" style="margin: 0;" disabled><template #prefix>@</template>{{ $t('proxyAccount') }}<template #desc>{{ $t('proxyAccountDescription') }}</template></mk-input>
|
||||
|
@ -176,7 +177,7 @@
|
|||
</div>
|
||||
</section>
|
||||
|
||||
<section class="_card">
|
||||
<section class="_card _vMargin">
|
||||
<div class="_title"><fa :icon="faBan"/> {{ $t('blockedInstances') }}</div>
|
||||
<div class="_content">
|
||||
<mk-textarea v-model="blockedHosts">
|
||||
|
@ -188,7 +189,7 @@
|
|||
</div>
|
||||
</section>
|
||||
|
||||
<section class="_card">
|
||||
<section class="_card _vMargin">
|
||||
<div class="_title"><fa :icon="faShareAlt"/> {{ $t('integration') }}</div>
|
||||
<div class="_content">
|
||||
<header><fa :icon="faTwitter"/> Twitter</header>
|
||||
|
@ -221,7 +222,8 @@
|
|||
<mk-button primary @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
|
||||
</div>
|
||||
</section>
|
||||
<section class="_card">
|
||||
|
||||
<section class="_card _vMargin">
|
||||
<div class="_title"><fa :icon="faArchway" /> Summaly Proxy</div>
|
||||
<div class="_content">
|
||||
<mk-input v-model="summalyProxy">URL</mk-input>
|
||||
|
@ -305,6 +307,7 @@ export default defineComponent({
|
|||
objectStorageSecretKey: null,
|
||||
objectStorageUseSSL: false,
|
||||
objectStorageUseProxy: false,
|
||||
objectStorageSetPublicRead: false,
|
||||
enableTwitterIntegration: false,
|
||||
twitterConsumerKey: null,
|
||||
twitterConsumerSecret: null,
|
||||
|
@ -372,6 +375,7 @@ export default defineComponent({
|
|||
this.objectStorageSecretKey = this.meta.objectStorageSecretKey;
|
||||
this.objectStorageUseSSL = this.meta.objectStorageUseSSL;
|
||||
this.objectStorageUseProxy = this.meta.objectStorageUseProxy;
|
||||
this.objectStorageSetPublicRead = this.meta.objectStorageSetPublicRead;
|
||||
this.enableTwitterIntegration = this.meta.enableTwitterIntegration;
|
||||
this.twitterConsumerKey = this.meta.twitterConsumerKey;
|
||||
this.twitterConsumerSecret = this.meta.twitterConsumerSecret;
|
||||
|
@ -521,6 +525,7 @@ export default defineComponent({
|
|||
objectStorageSecretKey: this.objectStorageSecretKey ? this.objectStorageSecretKey : null,
|
||||
objectStorageUseSSL: this.objectStorageUseSSL,
|
||||
objectStorageUseProxy: this.objectStorageUseProxy,
|
||||
objectStorageSetPublicRead: this.objectStorageSetPublicRead,
|
||||
enableTwitterIntegration: this.enableTwitterIntegration,
|
||||
twitterConsumerKey: this.twitterConsumerKey,
|
||||
twitterConsumerSecret: this.twitterConsumerSecret,
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<portal to="icon"><fa :icon="faUsers"/></portal>
|
||||
<portal to="title">{{ $t('users') }}</portal>
|
||||
|
||||
<section class="_card lookup">
|
||||
<section class="_card _vMargin lookup">
|
||||
<div class="_title"><fa :icon="faSearch"/> {{ $t('lookup') }}</div>
|
||||
<div class="_content">
|
||||
<mk-input class="target" v-model="target" type="text" @enter="showUser()">
|
||||
|
@ -16,7 +16,7 @@
|
|||
</div>
|
||||
</section>
|
||||
|
||||
<section class="_card users">
|
||||
<section class="_card _vMargin users">
|
||||
<div class="_title"><fa :icon="faUsers"/> {{ $t('users') }}</div>
|
||||
<div class="_content">
|
||||
<div class="inputs" style="display: flex;">
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div class="mk-messaging" v-size="[{ max: 400 }]">
|
||||
<div class="mk-messaging" v-size="{ max: [400] }">
|
||||
<portal to="icon"><fa :icon="faComments"/></portal>
|
||||
<portal to="title">{{ $t('messaging') }}</portal>
|
||||
|
||||
|
|
|
@ -53,7 +53,7 @@ export default defineComponent({
|
|||
};
|
||||
},
|
||||
computed: {
|
||||
draftId(): string {
|
||||
draftKey(): string {
|
||||
return this.user ? 'user:' + this.user.id : 'group:' + this.group.id;
|
||||
},
|
||||
canSend(): boolean {
|
||||
|
@ -79,7 +79,7 @@ export default defineComponent({
|
|||
autosize(this.$refs.text);
|
||||
|
||||
// 書きかけの投稿を復元
|
||||
const draft = JSON.parse(localStorage.getItem('message_drafts') || '{}')[this.draftId];
|
||||
const draft = JSON.parse(localStorage.getItem('message_drafts') || '{}')[this.draftKey];
|
||||
if (draft) {
|
||||
this.text = draft.data.text;
|
||||
this.file = draft.data.file;
|
||||
|
@ -199,7 +199,7 @@ export default defineComponent({
|
|||
saveDraft() {
|
||||
const data = JSON.parse(localStorage.getItem('message_drafts') || '{}');
|
||||
|
||||
data[this.draftId] = {
|
||||
data[this.draftKey] = {
|
||||
updatedAt: new Date(),
|
||||
data: {
|
||||
text: this.text,
|
||||
|
@ -213,7 +213,7 @@ export default defineComponent({
|
|||
deleteDraft() {
|
||||
const data = JSON.parse(localStorage.getItem('message_drafts') || '{}');
|
||||
|
||||
delete data[this.draftId];
|
||||
delete data[this.draftKey];
|
||||
|
||||
localStorage.setItem('message_drafts', JSON.stringify(data));
|
||||
},
|
||||
|
|
|
@ -1,22 +1,22 @@
|
|||
<template>
|
||||
<div v-if="$store.getters.isSignedIn">
|
||||
<div class="waiting _card" v-if="state == 'waiting'">
|
||||
<div class="waiting _card _vMargin" v-if="state == 'waiting'">
|
||||
<div class="_content">
|
||||
<mk-loading/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="denied _card" v-if="state == 'denied'">
|
||||
<div class="denied _card _vMargin" v-if="state == 'denied'">
|
||||
<div class="_content">
|
||||
<p>{{ $t('_auth.denied') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="accepted _card" v-else-if="state == 'accepted'">
|
||||
<div class="accepted _card _vMargin" v-else-if="state == 'accepted'">
|
||||
<div class="_content">
|
||||
<p v-if="callback">{{ $t('_auth.callback') }}<mk-ellipsis/></p>
|
||||
<p v-else>{{ $t('_auth.pleaseGoBack') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="_card" v-else>
|
||||
<div class="_card _vMargin" v-else>
|
||||
<div class="_title" v-if="name">{{ $t('_auth.shareAccess', { name: name }) }}</div>
|
||||
<div class="_title" v-else>{{ $t('_auth.shareAccessAsk') }}</div>
|
||||
<div class="_content">
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<portal to="title">{{ group.name }}</portal>
|
||||
|
||||
<transition name="zoom" mode="out-in">
|
||||
<div v-if="group" class="_card">
|
||||
<div v-if="group" class="_card _vMargin">
|
||||
<div class="_content">
|
||||
<mk-button inline @click="renameGroup()">{{ $t('rename') }}</mk-button>
|
||||
<mk-button inline @click="transfer()">{{ $t('transfer') }}</mk-button>
|
||||
|
@ -14,7 +14,7 @@
|
|||
</transition>
|
||||
|
||||
<transition name="zoom" mode="out-in">
|
||||
<div v-if="group" class="_card members">
|
||||
<div v-if="group" class="_card members _vMargin">
|
||||
<div class="_title">{{ $t('members') }}</div>
|
||||
<div class="_content">
|
||||
<div class="users">
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<portal to="title">{{ list.name }}</portal>
|
||||
|
||||
<transition name="zoom" mode="out-in">
|
||||
<div v-if="list" class="_card">
|
||||
<div v-if="list" class="_card _vMargin">
|
||||
<div class="_content">
|
||||
<mk-button inline @click="renameList()">{{ $t('rename') }}</mk-button>
|
||||
<mk-button inline @click="deleteList()">{{ $t('delete') }}</mk-button>
|
||||
|
@ -13,7 +13,7 @@
|
|||
</transition>
|
||||
|
||||
<transition name="zoom" mode="out-in">
|
||||
<div v-if="list" class="_card members">
|
||||
<div v-if="list" class="_card members _vMargin">
|
||||
<div class="_title">{{ $t('members') }}</div>
|
||||
<div class="_content">
|
||||
<div class="users">
|
||||
|
|
|
@ -3,11 +3,11 @@
|
|||
<portal to="icon"><fa :icon="faCog"/></portal>
|
||||
<portal to="title">{{ $t('accountSettings') }}</portal>
|
||||
|
||||
<x-profile-setting/>
|
||||
<x-privacy-setting/>
|
||||
<x-reaction-setting/>
|
||||
<x-profile-setting class="_vMargin"/>
|
||||
<x-privacy-setting class="_vMargin"/>
|
||||
<x-reaction-setting class="_vMargin"/>
|
||||
|
||||
<section class="_card">
|
||||
<section class="_card _vMargin">
|
||||
<div class="_title"><fa :icon="faCog"/> {{ $t('general') }}</div>
|
||||
<div class="_content">
|
||||
<mk-switch v-model="$store.state.i.autoWatch" @change="onChangeAutoWatch">
|
||||
|
@ -24,14 +24,14 @@
|
|||
</div>
|
||||
</section>
|
||||
|
||||
<x-import-export/>
|
||||
<x-drive/>
|
||||
<x-mute-block/>
|
||||
<x-word-mute/>
|
||||
<x-security/>
|
||||
<x-2fa/>
|
||||
<x-integration/>
|
||||
<x-api/>
|
||||
<x-import-export class="_vMargin"/>
|
||||
<x-drive class="_vMargin"/>
|
||||
<x-mute-block class="_vMargin"/>
|
||||
<x-word-mute class="_vMargin"/>
|
||||
<x-security class="_vMargin"/>
|
||||
<x-2fa class="_vMargin"/>
|
||||
<x-integration class="_vMargin"/>
|
||||
<x-api class="_vMargin"/>
|
||||
|
||||
<router-link class="_panel _buttonPrimary" to="/my/apps" style="margin: var(--margin) auto;">{{ $t('installedApps') }}</router-link>
|
||||
|
||||
|
|
|
@ -13,10 +13,11 @@
|
|||
</div>
|
||||
<div class="_content" v-show="tab === 'hard'">
|
||||
<mk-info>{{ $t('_wordMute.hardDescription') }}</mk-info>
|
||||
<mk-textarea v-model="hardMutedWords">
|
||||
<mk-textarea v-model="hardMutedWords" style="margin-bottom: 16px;">
|
||||
<span>{{ $t('_wordMute.muteWords') }}</span>
|
||||
<template #desc>{{ $t('_wordMute.muteWordsDescription') }}<br>{{ $t('_wordMute.muteWordsDescription2') }}</template>
|
||||
</mk-textarea>
|
||||
<div v-if="hardWordMutedNotesCount != null" class="_caption">{{ $t('_wordMute.mutedNotes') }}: {{ hardWordMutedNotesCount | number }}</div>
|
||||
</div>
|
||||
<div class="_footer">
|
||||
<mk-button @click="save()" primary inline :disabled="!changed"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
|
||||
|
@ -45,6 +46,7 @@ export default defineComponent({
|
|||
tab: 'soft',
|
||||
softMutedWords: '',
|
||||
hardMutedWords: '',
|
||||
hardWordMutedNotesCount: null,
|
||||
changed: false,
|
||||
faCommentSlash, faSave,
|
||||
}
|
||||
|
@ -59,9 +61,11 @@ export default defineComponent({
|
|||
},
|
||||
},
|
||||
|
||||
created() {
|
||||
async created() {
|
||||
this.softMutedWords = this.$store.state.settings.mutedWords.map(x => x.join(' ')).join('\n');
|
||||
this.hardMutedWords = this.$store.state.i.mutedWords.map(x => x.join(' ')).join('\n');
|
||||
|
||||
this.hardWordMutedNotesCount = (await this.$root.api('i/get-word-muted-notes-count', {})).count;
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
|
|
@ -5,13 +5,13 @@
|
|||
|
||||
<router-link v-if="$store.getters.isSignedIn" class="_panel _buttonPrimary" to="/my/settings" style="margin-bottom: var(--margin);">{{ $t('accountSettings') }}</router-link>
|
||||
|
||||
<x-theme/>
|
||||
<x-theme class="_vMargin"/>
|
||||
|
||||
<x-sidebar/>
|
||||
<x-sidebar class="_vMargin"/>
|
||||
|
||||
<x-plugins/>
|
||||
<x-plugins class="_vMargin"/>
|
||||
|
||||
<section class="_card">
|
||||
<section class="_card _vMargin">
|
||||
<div class="_title"><fa :icon="faMusic"/> {{ $t('sounds') }}</div>
|
||||
<div class="_content">
|
||||
<mk-range v-model="sfxVolume" :min="0" :max="1" :step="0.1">
|
||||
|
@ -50,10 +50,15 @@
|
|||
<option v-for="sound in sounds" :value="sound" :key="sound">{{ sound || $t('none') }}</option>
|
||||
<template #text><button class="_textButton" @click="listen(sfxAntenna)" v-if="sfxAntenna"><fa :icon="faPlay"/> {{ $t('listen') }}</button></template>
|
||||
</mk-select>
|
||||
<mk-select v-model="sfxChannel">
|
||||
<template #label>{{ $t('_sfx.channel') }}</template>
|
||||
<option v-for="sound in sounds" :value="sound" :key="sound">{{ sound || $t('none') }}</option>
|
||||
<template #text><button class="_textButton" @click="listen(sfxChannel)" v-if="sfxChannel"><fa :icon="faPlay"/> {{ $t('listen') }}</button></template>
|
||||
</mk-select>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="_card">
|
||||
<section class="_card _vMargin">
|
||||
<div class="_title"><fa :icon="faColumns"/> {{ $t('deck') }}</div>
|
||||
<div class="_content">
|
||||
<mk-switch v-model="deckAlwaysShowMainColumn">
|
||||
|
@ -67,7 +72,7 @@
|
|||
</div>
|
||||
</section>
|
||||
|
||||
<section class="_card">
|
||||
<section class="_card _vMargin">
|
||||
<div class="_title"><fa :icon="faCog"/> {{ $t('appearance') }}</div>
|
||||
<div class="_content">
|
||||
<mk-switch v-model="disableAnimatedMfm">{{ $t('disableAnimatedMfm') }}</mk-switch>
|
||||
|
@ -87,12 +92,13 @@
|
|||
</div>
|
||||
</section>
|
||||
|
||||
<section class="_card">
|
||||
<section class="_card _vMargin">
|
||||
<div class="_title"><fa :icon="faCog"/> {{ $t('general') }}</div>
|
||||
<div class="_content">
|
||||
<mk-switch v-model="autoReload">
|
||||
{{ $t('autoReloadWhenDisconnected') }}
|
||||
</mk-switch>
|
||||
<div>{{ $t('whenServerDisconnected') }}</div>
|
||||
<mk-radio v-model="serverDisconnectedBehavior" value="reload">{{ $t('_serverDisconnectedBehavior.reload') }}</mk-radio>
|
||||
<mk-radio v-model="serverDisconnectedBehavior" value="dialog">{{ $t('_serverDisconnectedBehavior.dialog') }}</mk-radio>
|
||||
<mk-radio v-model="serverDisconnectedBehavior" value="quiet">{{ $t('_serverDisconnectedBehavior.quiet') }}</mk-radio>
|
||||
</div>
|
||||
<div class="_content">
|
||||
<mk-switch v-model="imageNewTab">{{ $t('openImageInNewTab') }}</mk-switch>
|
||||
|
@ -142,10 +148,14 @@ const sounds = [
|
|||
'syuilo/pirori',
|
||||
'syuilo/pirori-wet',
|
||||
'syuilo/pirori-square-wet',
|
||||
'syuilo/square-pico',
|
||||
'syuilo/reverved',
|
||||
'syuilo/ryukyu',
|
||||
'aisha/1',
|
||||
'aisha/2',
|
||||
'aisha/3',
|
||||
'noizenecio/kick_gaba',
|
||||
'noizenecio/kick_gaba2',
|
||||
];
|
||||
|
||||
export default defineComponent({
|
||||
|
@ -177,9 +187,9 @@ export default defineComponent({
|
|||
},
|
||||
|
||||
computed: {
|
||||
autoReload: {
|
||||
get() { return this.$store.state.device.autoReload; },
|
||||
set(value) { this.$store.commit('device/set', { key: 'autoReload', value }); }
|
||||
serverDisconnectedBehavior: {
|
||||
get() { return this.$store.state.device.serverDisconnectedBehavior; },
|
||||
set(value) { this.$store.commit('device/set', { key: 'serverDisconnectedBehavior', value }); }
|
||||
},
|
||||
|
||||
reduceAnimation: {
|
||||
|
@ -272,6 +282,11 @@ export default defineComponent({
|
|||
set(value) { this.$store.commit('device/set', { key: 'sfxAntenna', value }); }
|
||||
},
|
||||
|
||||
sfxChannel: {
|
||||
get() { return this.$store.state.device.sfxChannel; },
|
||||
set(value) { this.$store.commit('device/set', { key: 'sfxChannel', value }); }
|
||||
},
|
||||
|
||||
volumeIcon: {
|
||||
get() {
|
||||
return this.sfxVolume === 0 ? faVolumeMute : faVolumeUp;
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
/>
|
||||
</portal>
|
||||
|
||||
<div class="controller _card" v-if="objectSelected">
|
||||
<div class="controller _card _vMargin" v-if="objectSelected">
|
||||
<div class="_content">
|
||||
<p class="name">{{ selectedFurnitureName }}</p>
|
||||
<x-preview ref="preview"/>
|
||||
|
@ -34,7 +34,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="menu _card" v-if="isMyRoom">
|
||||
<div class="menu _card _vMargin" v-if="isMyRoom">
|
||||
<div class="_content">
|
||||
<mk-button @click="add()"><fa :icon="faBoxOpen"/> {{ $t('_rooms.addFurniture') }}</mk-button>
|
||||
</div>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div class="kjeftjfm" v-size="[{ max: 500 }]">
|
||||
<div class="kjeftjfm" v-size="{ max: [500] }">
|
||||
<div class="with">
|
||||
<button class="_button" @click="with_ = null" :class="{ active: with_ === null }">{{ $t('notes') }}</button>
|
||||
<button class="_button" @click="with_ = 'replies'" :class="{ active: with_ === 'replies' }">{{ $t('notesAndReplies') }}</button>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div class="mk-user-page" v-if="user" v-size="[{ max: 500 }]">
|
||||
<div class="mk-user-page" v-if="user" v-size="{ max: [500] }">
|
||||
<portal to="title" v-if="user"><mk-user-name :user="user" :nowrap="false" class="name"/></portal>
|
||||
<portal to="avatar" v-if="user"><mk-avatar class="avatar" :user="user" :disable-preview="true"/></portal>
|
||||
|
||||
|
|
|
@ -27,6 +27,10 @@ export const router = createRouter({
|
|||
{ path: '/explore', component: page('explore') },
|
||||
{ path: '/explore/tags/:tag', props: true, component: page('explore') },
|
||||
{ path: '/search', component: page('search') },
|
||||
{ path: '/channels', component: page('channels') },
|
||||
{ path: '/channels/new', component: page('channel-editor') },
|
||||
{ path: '/channels/:channelId/edit', component: page('channel-editor'), props: true },
|
||||
{ path: '/channels/:channelId', component: page('channel'), props: true },
|
||||
{ path: '/my/notifications', component: page('notifications') },
|
||||
{ path: '/my/favorites', component: page('favorites') },
|
||||
{ path: '/my/messages', component: page('messages') },
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { getScrollPosition, onScrollTop } from './scroll';
|
||||
import { onScrollTop, isTopVisible } from './scroll';
|
||||
|
||||
const SECOND_FETCH_LIMIT = 30;
|
||||
|
||||
|
@ -147,7 +147,7 @@ export default (opts) => ({
|
|||
},
|
||||
|
||||
prepend(item) {
|
||||
const isTop = this.isBackTop || (document.body.contains(this.$el) && (getScrollPosition(this.$el) === 0));
|
||||
const isTop = this.isBackTop || (document.body.contains(this.$el) && isTopVisible(this.$el));
|
||||
|
||||
if (isTop) {
|
||||
// Prepend the item
|
||||
|
|
|
@ -13,14 +13,20 @@ export function getScrollPosition(el: Element | null): number {
|
|||
return container == null ? window.scrollY : container.scrollTop;
|
||||
}
|
||||
|
||||
export function isTopVisible(el: Element | null): boolean {
|
||||
const scrollTop = getScrollPosition(el);
|
||||
const topPosition = el.offsetTop; // TODO: container内でのelの相対位置を取得できればより正確になる
|
||||
|
||||
return scrollTop <= topPosition;
|
||||
}
|
||||
|
||||
export function onScrollTop(el: Element, cb) {
|
||||
const container = getScrollContainer(el) || window;
|
||||
const onScroll = ev => {
|
||||
if (!document.body.contains(el)) return;
|
||||
const pos = getScrollPosition(el);
|
||||
if (pos === 0) {
|
||||
if (isTopVisible(el)) {
|
||||
cb();
|
||||
container.removeEventListener('scroll', onscroll);
|
||||
container.removeEventListener('scroll', onScroll);
|
||||
}
|
||||
};
|
||||
container.addEventListener('scroll', onScroll, { passive: true });
|
||||
|
@ -33,7 +39,7 @@ export function onScrollBottom(el: Element, cb) {
|
|||
const pos = getScrollPosition(el);
|
||||
if (pos + el.clientHeight > el.scrollHeight - 1) {
|
||||
cb();
|
||||
container.removeEventListener('scroll', onscroll);
|
||||
container.removeEventListener('scroll', onScroll);
|
||||
}
|
||||
};
|
||||
container.addEventListener('scroll', onScroll, { passive: true });
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { createStore } from 'vuex';
|
||||
import createPersistedState from 'vuex-persistedstate';
|
||||
import * as nestedProperty from 'nested-property';
|
||||
import { faTerminal, faHashtag, faBroadcastTower, faFireAlt, faSearch, faStar, faAt, faListUl, faUserClock, faUsers, faCloud, faGamepad, faFileAlt, faSatellite, faDoorClosed, faColumns } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faSatelliteDish, faTerminal, faHashtag, faBroadcastTower, faFireAlt, faSearch, faStar, faAt, faListUl, faUserClock, faUsers, faCloud, faGamepad, faFileAlt, faSatellite, faDoorClosed, faColumns } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faBell, faEnvelope, faComments } from '@fortawesome/free-regular-svg-icons';
|
||||
import { AiScript, utils, values } from '@syuilo/aiscript';
|
||||
import { apiUrl, deckmode } from './config';
|
||||
|
@ -60,7 +60,7 @@ export const defaultDeviceSettings = {
|
|||
loadRawImages: false,
|
||||
alwaysShowNsfw: false,
|
||||
useOsNativeEmojis: false,
|
||||
autoReload: false,
|
||||
serverDisconnectedBehavior: 'quiet',
|
||||
accounts: [],
|
||||
recentEmojis: [],
|
||||
themes: [],
|
||||
|
@ -90,6 +90,7 @@ export const defaultDeviceSettings = {
|
|||
sfxChat: 'syuilo/pope1',
|
||||
sfxChatBg: 'syuilo/waon',
|
||||
sfxAntenna: 'syuilo/triple',
|
||||
sfxChannel: 'syuilo/square-pico',
|
||||
userData: {},
|
||||
};
|
||||
|
||||
|
@ -113,6 +114,7 @@ export const store = createStore({
|
|||
text: string;
|
||||
result: any;
|
||||
}[],
|
||||
fullView: false,
|
||||
|
||||
// Plugin
|
||||
pluginContexts: new Map<string, AiScript>(),
|
||||
|
@ -219,6 +221,11 @@ export const store = createStore({
|
|||
get show() { return getters.isSignedIn; },
|
||||
to: '/my/pages',
|
||||
},
|
||||
channels: {
|
||||
title: 'channel',
|
||||
icon: faSatelliteDish,
|
||||
to: '/channels',
|
||||
},
|
||||
games: {
|
||||
title: 'games',
|
||||
icon: faGamepad,
|
||||
|
@ -259,6 +266,10 @@ export const store = createStore({
|
|||
state.dialogs.push(dialog);
|
||||
},
|
||||
|
||||
setFullView(state, v) {
|
||||
state.fullView = v;
|
||||
},
|
||||
|
||||
initPlugin(state, { plugin, aiscript }) {
|
||||
state.pluginContexts.set(plugin.id, aiscript);
|
||||
},
|
||||
|
|
|
@ -329,10 +329,6 @@ hr {
|
|||
._card {
|
||||
@extend ._panel;
|
||||
|
||||
& + ._card {
|
||||
margin-top: var(--margin);
|
||||
}
|
||||
|
||||
> ._title {
|
||||
margin: 0;
|
||||
padding: 22px 32px;
|
||||
|
@ -389,6 +385,40 @@ hr {
|
|||
}
|
||||
}
|
||||
|
||||
._vMargin {
|
||||
& + ._vMargin {
|
||||
margin-top: var(--margin);
|
||||
}
|
||||
}
|
||||
|
||||
._table {
|
||||
> ._row {
|
||||
display: flex;
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-bottom: 16px;
|
||||
|
||||
@media (max-width: 500px) {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
> ._cell {
|
||||
flex: 1;
|
||||
|
||||
> ._label {
|
||||
font-size: 80%;
|
||||
opacity: 0.7;
|
||||
|
||||
> ._icon {
|
||||
margin-right: 4px;
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
._fullinfo {
|
||||
padding: 64px 32px;
|
||||
text-align: center;
|
||||
|
@ -404,7 +434,7 @@ hr {
|
|||
._keyValue {
|
||||
display: flex;
|
||||
|
||||
> div {
|
||||
> * {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
@ -413,6 +443,11 @@ hr {
|
|||
color: var(--link);
|
||||
}
|
||||
|
||||
._caption {
|
||||
font-size: 0.8em;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.zoom-enter-active, .zoom-leave-active {
|
||||
transition: opacity 0.5s, transform 0.5s !important;
|
||||
}
|
||||
|
|
|
@ -72,5 +72,6 @@
|
|||
X12: 'rgba(255, 255, 255, 0.1)',
|
||||
X13: 'rgba(255, 255, 255, 0.15)',
|
||||
X14: ':alpha<0.5<@navBg',
|
||||
X15: ':alpha<0<@panel',
|
||||
},
|
||||
}
|
||||
|
|
|
@ -72,5 +72,6 @@
|
|||
X12: 'rgba(0, 0, 0, 0.1)',
|
||||
X13: 'rgba(0, 0, 0, 0.15)',
|
||||
X14: ':alpha<0.5<@navBg',
|
||||
X15: ':alpha<0<@panel',
|
||||
},
|
||||
}
|
||||
|
|
|
@ -8,7 +8,8 @@ export default function <T extends Form>(data: {
|
|||
return defineComponent({
|
||||
props: {
|
||||
widget: {
|
||||
type: Object
|
||||
type: Object,
|
||||
required: false
|
||||
},
|
||||
isCustomizeMode: {
|
||||
type: Boolean,
|
||||
|
@ -18,11 +19,11 @@ export default function <T extends Form>(data: {
|
|||
|
||||
computed: {
|
||||
id(): string {
|
||||
return this.widget.id;
|
||||
return this.widget ? this.widget.id : null;
|
||||
},
|
||||
|
||||
props(): Record<string, any> {
|
||||
return this.widget.data;
|
||||
return this.widget ? this.widget.data : {};
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -61,8 +62,10 @@ export default function <T extends Form>(data: {
|
|||
},
|
||||
|
||||
save() {
|
||||
if (this.widget) {
|
||||
this.$store.commit('deviceUser/updateWidget', this.widget);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<mk-container :show-header="props.showHeader">
|
||||
<mk-container :show-header="props.showHeader" :body-togglable="bodyTogglable" :scrollable="scrollable">
|
||||
<template #header><fa :icon="faGlobe"/>{{ $t('_widgets.federation') }}</template>
|
||||
|
||||
<div class="wbrkwalb">
|
||||
|
@ -40,6 +40,18 @@ export default defineComponent({
|
|||
components: {
|
||||
MkContainer, MkMiniChart
|
||||
},
|
||||
props: {
|
||||
bodyTogglable: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
scrollable: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
instances: [],
|
||||
|
|
|
@ -38,7 +38,7 @@ import { FollowRequest } from '../models/entities/follow-request';
|
|||
import { Emoji } from '../models/entities/emoji';
|
||||
import { ReversiGame } from '../models/entities/games/reversi/game';
|
||||
import { ReversiMatching } from '../models/entities/games/reversi/matching';
|
||||
import { UserNotePining } from '../models/entities/user-note-pinings';
|
||||
import { UserNotePining } from '../models/entities/user-note-pining';
|
||||
import { Poll } from '../models/entities/poll';
|
||||
import { UserKeypair } from '../models/entities/user-keypair';
|
||||
import { UserPublickey } from '../models/entities/user-publickey';
|
||||
|
@ -60,6 +60,9 @@ import { PromoRead } from '../models/entities/promo-read';
|
|||
import { program } from '../argv';
|
||||
import { Relay } from '../models/entities/relay';
|
||||
import { MutedNote } from '../models/entities/muted-note';
|
||||
import { Channel } from '../models/entities/channel';
|
||||
import { ChannelFollowing } from '../models/entities/channel-following';
|
||||
import { ChannelNotePining } from '../models/entities/channel-note-pining';
|
||||
|
||||
const sqlLogger = dbLogger.createSubLogger('sql', 'white', false);
|
||||
|
||||
|
@ -153,6 +156,9 @@ export const entities = [
|
|||
ReversiMatching,
|
||||
Relay,
|
||||
MutedNote,
|
||||
Channel,
|
||||
ChannelFollowing,
|
||||
ChannelNotePining,
|
||||
...charts as any
|
||||
];
|
||||
|
||||
|
|
|
@ -25,4 +25,6 @@ export const kinds = [
|
|||
'read:page-likes',
|
||||
'read:user-groups',
|
||||
'write:user-groups',
|
||||
'read:channels',
|
||||
'write:channels',
|
||||
];
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
|
||||
import { User } from './user';
|
||||
import { id } from '../id';
|
||||
import { Channel } from './channel';
|
||||
|
||||
@Entity()
|
||||
@Index(['followerId', 'followeeId'], { unique: true })
|
||||
export class ChannelFollowing {
|
||||
@PrimaryColumn(id())
|
||||
public id: string;
|
||||
|
||||
@Index()
|
||||
@Column('timestamp with time zone', {
|
||||
comment: 'The created date of the ChannelFollowing.'
|
||||
})
|
||||
public createdAt: Date;
|
||||
|
||||
@Index()
|
||||
@Column({
|
||||
...id(),
|
||||
comment: 'The followee channel ID.'
|
||||
})
|
||||
public followeeId: Channel['id'];
|
||||
|
||||
@ManyToOne(type => Channel, {
|
||||
onDelete: 'CASCADE'
|
||||
})
|
||||
@JoinColumn()
|
||||
public followee: Channel | null;
|
||||
|
||||
@Index()
|
||||
@Column({
|
||||
...id(),
|
||||
comment: 'The follower user ID.'
|
||||
})
|
||||
public followerId: User['id'];
|
||||
|
||||
@ManyToOne(type => User, {
|
||||
onDelete: 'CASCADE'
|
||||
})
|
||||
@JoinColumn()
|
||||
public follower: User | null;
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
|
||||
import { Note } from './note';
|
||||
import { Channel } from './channel';
|
||||
import { id } from '../id';
|
||||
|
||||
@Entity()
|
||||
@Index(['channelId', 'noteId'], { unique: true })
|
||||
export class ChannelNotePining {
|
||||
@PrimaryColumn(id())
|
||||
public id: string;
|
||||
|
||||
@Column('timestamp with time zone', {
|
||||
comment: 'The created date of the ChannelNotePining.'
|
||||
})
|
||||
public createdAt: Date;
|
||||
|
||||
@Index()
|
||||
@Column(id())
|
||||
public channelId: Channel['id'];
|
||||
|
||||
@ManyToOne(type => Channel, {
|
||||
onDelete: 'CASCADE'
|
||||
})
|
||||
@JoinColumn()
|
||||
public channel: Channel | null;
|
||||
|
||||
@Column(id())
|
||||
public noteId: Note['id'];
|
||||
|
||||
@ManyToOne(type => Note, {
|
||||
onDelete: 'CASCADE'
|
||||
})
|
||||
@JoinColumn()
|
||||
public note: Note | null;
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
|
||||
import { User } from './user';
|
||||
import { id } from '../id';
|
||||
import { DriveFile } from './drive-file';
|
||||
|
||||
@Entity()
|
||||
export class Channel {
|
||||
@PrimaryColumn(id())
|
||||
public id: string;
|
||||
|
||||
@Index()
|
||||
@Column('timestamp with time zone', {
|
||||
comment: 'The created date of the Channel.'
|
||||
})
|
||||
public createdAt: Date;
|
||||
|
||||
@Index()
|
||||
@Column('timestamp with time zone', {
|
||||
nullable: true
|
||||
})
|
||||
public lastNotedAt: Date | null;
|
||||
|
||||
@Index()
|
||||
@Column({
|
||||
...id(),
|
||||
comment: 'The owner ID.'
|
||||
})
|
||||
public userId: User['id'];
|
||||
|
||||
@ManyToOne(type => User, {
|
||||
onDelete: 'SET NULL'
|
||||
})
|
||||
@JoinColumn()
|
||||
public user: User | null;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 128,
|
||||
comment: 'The name of the Channel.'
|
||||
})
|
||||
public name: string;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 2048, nullable: true,
|
||||
comment: 'The description of the Channel.'
|
||||
})
|
||||
public description: string | null;
|
||||
|
||||
@Column({
|
||||
...id(),
|
||||
nullable: true,
|
||||
comment: 'The ID of banner Channel.'
|
||||
})
|
||||
public bannerId: DriveFile['id'] | null;
|
||||
|
||||
@ManyToOne(type => DriveFile, {
|
||||
onDelete: 'SET NULL'
|
||||
})
|
||||
@JoinColumn()
|
||||
public banner: DriveFile | null;
|
||||
|
||||
@Index()
|
||||
@Column('integer', {
|
||||
default: 0,
|
||||
comment: 'The count of notes.'
|
||||
})
|
||||
public notesCount: number;
|
||||
|
||||
@Index()
|
||||
@Column('integer', {
|
||||
default: 0,
|
||||
comment: 'The count of users.'
|
||||
})
|
||||
public usersCount: number;
|
||||
}
|
|
@ -145,6 +145,7 @@ export class DriveFile {
|
|||
@JoinColumn()
|
||||
public folder: DriveFolder | null;
|
||||
|
||||
@Index()
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
comment: 'Whether the DriveFile is NSFW.'
|
||||
|
|
|
@ -370,4 +370,9 @@ export class Meta {
|
|||
default: true,
|
||||
})
|
||||
public objectStorageUseProxy: boolean;
|
||||
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
})
|
||||
public objectStorageSetPublicRead: boolean;
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typ
|
|||
import { User } from './user';
|
||||
import { Note } from './note';
|
||||
import { id } from '../id';
|
||||
import { Channel } from './channel';
|
||||
|
||||
@Entity()
|
||||
@Index(['userId', 'noteId'], { unique: true })
|
||||
|
@ -29,15 +30,34 @@ export class NoteUnread {
|
|||
@JoinColumn()
|
||||
public note: Note | null;
|
||||
|
||||
/**
|
||||
* メンションか否か
|
||||
*/
|
||||
@Index()
|
||||
@Column('boolean')
|
||||
public isMentioned: boolean;
|
||||
|
||||
/**
|
||||
* ダイレクト投稿か否か
|
||||
*/
|
||||
@Index()
|
||||
@Column('boolean')
|
||||
public isSpecified: boolean;
|
||||
|
||||
//#region Denormalized fields
|
||||
@Index()
|
||||
@Column({
|
||||
...id(),
|
||||
comment: '[Denormalized]'
|
||||
})
|
||||
public noteUserId: User['id'];
|
||||
|
||||
/**
|
||||
* ダイレクト投稿か
|
||||
*/
|
||||
@Column('boolean')
|
||||
public isSpecified: boolean;
|
||||
@Index()
|
||||
@Column({
|
||||
...id(),
|
||||
nullable: true,
|
||||
comment: '[Denormalized]'
|
||||
})
|
||||
public noteChannelId: Channel['id'] | null;
|
||||
//#endregion
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ import { User } from './user';
|
|||
import { DriveFile } from './drive-file';
|
||||
import { id } from '../id';
|
||||
import { noteVisibilities } from '../../types';
|
||||
|
||||
import { Channel } from './channel';
|
||||
|
||||
@Entity()
|
||||
@Index('IDX_NOTE_TAGS', { synchronize: false })
|
||||
|
@ -173,6 +173,20 @@ export class Note {
|
|||
})
|
||||
public hasPoll: boolean;
|
||||
|
||||
@Index()
|
||||
@Column({
|
||||
...id(),
|
||||
nullable: true, default: null,
|
||||
comment: 'The ID of source channel.'
|
||||
})
|
||||
public channelId: Channel['id'] | null;
|
||||
|
||||
@ManyToOne(type => Channel, {
|
||||
onDelete: 'CASCADE'
|
||||
})
|
||||
@JoinColumn()
|
||||
public channel: Channel | null;
|
||||
|
||||
//#region Denormalized fields
|
||||
@Index()
|
||||
@Column('varchar', {
|
||||
|
|
|
@ -15,7 +15,7 @@ import { DriveFileRepository } from './repositories/drive-file';
|
|||
import { DriveFolderRepository } from './repositories/drive-folder';
|
||||
import { Log } from './entities/log';
|
||||
import { AccessToken } from './entities/access-token';
|
||||
import { UserNotePining } from './entities/user-note-pinings';
|
||||
import { UserNotePining } from './entities/user-note-pining';
|
||||
import { SigninRepository } from './repositories/signin';
|
||||
import { MessagingMessageRepository } from './repositories/messaging-message';
|
||||
import { ReversiGameRepository } from './repositories/games/reversi/game';
|
||||
|
@ -53,7 +53,10 @@ import { PromoNote } from './entities/promo-note';
|
|||
import { PromoRead } from './entities/promo-read';
|
||||
import { EmojiRepository } from './repositories/emoji';
|
||||
import { RelayRepository } from './repositories/relay';
|
||||
import { ChannelRepository } from './repositories/channel';
|
||||
import { MutedNote } from './entities/muted-note';
|
||||
import { ChannelFollowing } from './entities/channel-following';
|
||||
import { ChannelNotePining } from './entities/channel-note-pining';
|
||||
|
||||
export const Announcements = getRepository(Announcement);
|
||||
export const AnnouncementReads = getRepository(AnnouncementRead);
|
||||
|
@ -110,3 +113,6 @@ export const PromoNotes = getRepository(PromoNote);
|
|||
export const PromoReads = getRepository(PromoRead);
|
||||
export const Relays = getCustomRepository(RelayRepository);
|
||||
export const MutedNotes = getRepository(MutedNote);
|
||||
export const Channels = getCustomRepository(ChannelRepository);
|
||||
export const ChannelFollowings = getRepository(ChannelFollowing);
|
||||
export const ChannelNotePinings = getRepository(ChannelNotePining);
|
||||
|
|
|
@ -0,0 +1,101 @@
|
|||
import { EntityRepository, Repository } from 'typeorm';
|
||||
import { Channel } from '../entities/channel';
|
||||
import { ensure } from '../../prelude/ensure';
|
||||
import { SchemaType } from '../../misc/schema';
|
||||
import { DriveFiles, ChannelFollowings, NoteUnreads } from '..';
|
||||
import { User } from '../entities/user';
|
||||
|
||||
export type PackedChannel = SchemaType<typeof packedChannelSchema>;
|
||||
|
||||
@EntityRepository(Channel)
|
||||
export class ChannelRepository extends Repository<Channel> {
|
||||
public async pack(
|
||||
src: Channel['id'] | Channel,
|
||||
me?: User['id'] | User | null | undefined,
|
||||
): Promise<PackedChannel> {
|
||||
const channel = typeof src === 'object' ? src : await this.findOne(src).then(ensure);
|
||||
const meId = me ? typeof me === 'string' ? me : me.id : null;
|
||||
|
||||
const banner = channel.bannerId ? await DriveFiles.findOne(channel.bannerId) : null;
|
||||
|
||||
const hasUnreadNote = me ? (await NoteUnreads.findOne({ noteChannelId: channel.id, userId: meId })) != null : undefined;
|
||||
|
||||
const following = await ChannelFollowings.findOne({
|
||||
followerId: meId,
|
||||
followeeId: channel.id,
|
||||
});
|
||||
|
||||
return {
|
||||
id: channel.id,
|
||||
createdAt: channel.createdAt.toISOString(),
|
||||
lastNotedAt: channel.lastNotedAt ? channel.lastNotedAt.toISOString() : null,
|
||||
name: channel.name,
|
||||
description: channel.description,
|
||||
userId: channel.userId,
|
||||
bannerUrl: banner ? DriveFiles.getPublicUrl(banner, false) : null,
|
||||
usersCount: channel.usersCount,
|
||||
notesCount: channel.notesCount,
|
||||
|
||||
...(me ? {
|
||||
isFollowing: following != null,
|
||||
hasUnreadNote,
|
||||
} : {})
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const packedChannelSchema = {
|
||||
type: 'object' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
properties: {
|
||||
id: {
|
||||
type: 'string' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
format: 'id',
|
||||
description: 'The unique identifier for this Channel.',
|
||||
example: 'xxxxxxxxxx',
|
||||
},
|
||||
createdAt: {
|
||||
type: 'string' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
format: 'date-time',
|
||||
description: 'The date that the Channel was created.'
|
||||
},
|
||||
lastNotedAt: {
|
||||
type: 'string' as const,
|
||||
optional: false as const, nullable: true as const,
|
||||
format: 'date-time',
|
||||
},
|
||||
name: {
|
||||
type: 'string' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
description: 'The name of the Channel.'
|
||||
},
|
||||
description: {
|
||||
type: 'string' as const,
|
||||
nullable: true as const, optional: false as const,
|
||||
},
|
||||
bannerUrl: {
|
||||
type: 'string' as const,
|
||||
format: 'url',
|
||||
nullable: true as const, optional: false as const,
|
||||
},
|
||||
notesCount: {
|
||||
type: 'number' as const,
|
||||
nullable: false as const, optional: false as const,
|
||||
},
|
||||
usersCount: {
|
||||
type: 'number' as const,
|
||||
nullable: false as const, optional: false as const,
|
||||
},
|
||||
isFollowing: {
|
||||
type: 'boolean' as const,
|
||||
optional: true as const, nullable: false as const,
|
||||
},
|
||||
userId: {
|
||||
type: 'string' as const,
|
||||
nullable: false as const, optional: false as const,
|
||||
format: 'id',
|
||||
},
|
||||
},
|
||||
};
|
|
@ -1,7 +1,7 @@
|
|||
import { EntityRepository, Repository, In } from 'typeorm';
|
||||
import { Note } from '../entities/note';
|
||||
import { User } from '../entities/user';
|
||||
import { Emojis, Users, PollVotes, DriveFiles, NoteReactions, Followings, Polls } from '..';
|
||||
import { Emojis, Users, PollVotes, DriveFiles, NoteReactions, Followings, Polls, Channels } from '..';
|
||||
import { ensure } from '../../prelude/ensure';
|
||||
import { SchemaType } from '../../misc/schema';
|
||||
import { awaitAll } from '../../prelude/await-all';
|
||||
|
@ -207,6 +207,12 @@ export class NoteRepository extends Repository<Note> {
|
|||
text = `【${note.name}】\n${(note.text || '').trim()}\n\n${note.url || note.uri}`;
|
||||
}
|
||||
|
||||
const channel = note.channelId
|
||||
? note.channel
|
||||
? note.channel
|
||||
: await Channels.findOne(note.channelId)
|
||||
: null;
|
||||
|
||||
const packed = await awaitAll({
|
||||
id: note.id,
|
||||
createdAt: note.createdAt.toISOString(),
|
||||
|
@ -227,6 +233,11 @@ export class NoteRepository extends Repository<Note> {
|
|||
files: DriveFiles.packMany(note.fileIds),
|
||||
replyId: note.replyId,
|
||||
renoteId: note.renoteId,
|
||||
channelId: note.channelId || undefined,
|
||||
channel: channel ? {
|
||||
id: channel.id,
|
||||
name: channel.name,
|
||||
} : undefined,
|
||||
mentions: note.mentions.length > 0 ? note.mentions : undefined,
|
||||
uri: note.uri || undefined,
|
||||
url: note.url || undefined,
|
||||
|
@ -391,6 +402,16 @@ export const packedNoteSchema = {
|
|||
type: 'object' as const,
|
||||
optional: true as const, nullable: true as const,
|
||||
},
|
||||
|
||||
channelId: {
|
||||
type: 'string' as const,
|
||||
optional: true as const, nullable: true as const,
|
||||
format: 'id',
|
||||
example: 'xxxxxxxxxx',
|
||||
},
|
||||
channel: {
|
||||
type: 'object' as const,
|
||||
optional: true as const, nullable: true as const,
|
||||
ref: 'Channel'
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import $ from 'cafy';
|
||||
import { EntityRepository, Repository, In, Not } from 'typeorm';
|
||||
import { User, ILocalUser, IRemoteUser } from '../entities/user';
|
||||
import { Emojis, Notes, NoteUnreads, FollowRequests, Notifications, MessagingMessages, UserNotePinings, Followings, Blockings, Mutings, UserProfiles, UserSecurityKeys, UserGroupJoinings, Pages, Announcements, AnnouncementReads, Antennas, AntennaNotes } from '..';
|
||||
import { Emojis, Notes, NoteUnreads, FollowRequests, Notifications, MessagingMessages, UserNotePinings, Followings, Blockings, Mutings, UserProfiles, UserSecurityKeys, UserGroupJoinings, Pages, Announcements, AnnouncementReads, Antennas, AntennaNotes, ChannelFollowings } from '..';
|
||||
import { ensure } from '../../prelude/ensure';
|
||||
import config from '../../config';
|
||||
import { SchemaType } from '../../misc/schema';
|
||||
|
@ -107,6 +107,17 @@ export class UserRepository extends Repository<User> {
|
|||
return unread != null;
|
||||
}
|
||||
|
||||
public async getHasUnreadChannel(userId: User['id']): Promise<boolean> {
|
||||
const channels = await ChannelFollowings.find({ followerId: userId });
|
||||
|
||||
const unread = channels.length > 0 ? await NoteUnreads.findOne({
|
||||
userId: userId,
|
||||
noteChannelId: In(channels.map(x => x.id)),
|
||||
}) : null;
|
||||
|
||||
return unread != null;
|
||||
}
|
||||
|
||||
public async getHasUnreadNotification(userId: User['id']): Promise<boolean> {
|
||||
const mute = await Mutings.find({
|
||||
muterId: userId
|
||||
|
@ -139,7 +150,6 @@ export class UserRepository extends Repository<User> {
|
|||
options?: {
|
||||
detail?: boolean,
|
||||
includeSecrets?: boolean,
|
||||
includeHasUnreadNotes?: boolean
|
||||
}
|
||||
): Promise<PackedUser> {
|
||||
const opts = Object.assign({
|
||||
|
@ -181,17 +191,6 @@ export class UserRepository extends Repository<User> {
|
|||
select: ['name', 'host', 'url', 'aliases']
|
||||
}) : [],
|
||||
|
||||
...(opts.includeHasUnreadNotes ? {
|
||||
hasUnreadSpecifiedNotes: NoteUnreads.count({
|
||||
where: { userId: user.id, isSpecified: true },
|
||||
take: 1
|
||||
}).then(count => count > 0),
|
||||
hasUnreadMentions: NoteUnreads.count({
|
||||
where: { userId: user.id },
|
||||
take: 1
|
||||
}).then(count => count > 0),
|
||||
} : {}),
|
||||
|
||||
...(opts.detail ? {
|
||||
url: profile!.url,
|
||||
createdAt: user.createdAt.toISOString(),
|
||||
|
@ -233,8 +232,17 @@ export class UserRepository extends Repository<User> {
|
|||
alwaysMarkNsfw: profile!.alwaysMarkNsfw,
|
||||
carefulBot: profile!.carefulBot,
|
||||
autoAcceptFollowed: profile!.autoAcceptFollowed,
|
||||
hasUnreadSpecifiedNotes: NoteUnreads.count({
|
||||
where: { userId: user.id, isSpecified: true },
|
||||
take: 1
|
||||
}).then(count => count > 0),
|
||||
hasUnreadMentions: NoteUnreads.count({
|
||||
where: { userId: user.id, isMentioned: true },
|
||||
take: 1
|
||||
}).then(count => count > 0),
|
||||
hasUnreadAnnouncement: this.getHasUnreadAnnouncement(user.id),
|
||||
hasUnreadAntenna: this.getHasUnreadAntenna(user.id),
|
||||
hasUnreadChannel: this.getHasUnreadChannel(user.id),
|
||||
hasUnreadMessagingMessage: this.getHasUnreadMessagingMessage(user.id),
|
||||
hasUnreadNotification: this.getHasUnreadNotification(user.id),
|
||||
hasPendingReceivedFollowRequest: this.getHasPendingReceivedFollowRequest(user.id),
|
||||
|
@ -276,7 +284,6 @@ export class UserRepository extends Repository<User> {
|
|||
options?: {
|
||||
detail?: boolean,
|
||||
includeSecrets?: boolean,
|
||||
includeHasUnreadNotes?: boolean
|
||||
}
|
||||
) {
|
||||
return Promise.all(users.map(u => this.pack(u, me, options)));
|
||||
|
|
|
@ -15,7 +15,7 @@ import { updateUsertags } from '../../../services/update-hashtag';
|
|||
import { Users, UserNotePinings, Instances, DriveFiles, Followings, UserProfiles, UserPublickeys } from '../../../models';
|
||||
import { User, IRemoteUser } from '../../../models/entities/user';
|
||||
import { Emoji } from '../../../models/entities/emoji';
|
||||
import { UserNotePining } from '../../../models/entities/user-note-pinings';
|
||||
import { UserNotePining } from '../../../models/entities/user-note-pining';
|
||||
import { genId } from '../../../misc/gen-id';
|
||||
import { instanceChart, usersChart } from '../../../services/chart';
|
||||
import { UserPublickey } from '../../../models/entities/user-publickey';
|
||||
|
|
|
@ -49,7 +49,7 @@ export default async (user: ILocalUser, url: string, object: any) => {
|
|||
authorizationHeaderName: 'Signature',
|
||||
key: keypair.privateKey,
|
||||
keyId: `${config.url}/users/${user.id}#main-key`,
|
||||
headers: ['date', 'host', 'digest']
|
||||
headers: ['(request-target)', 'date', 'host', 'digest']
|
||||
});
|
||||
|
||||
req.on('timeout', () => req.abort());
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
import { User } from '../../../models/entities/user';
|
||||
import { ChannelFollowings } from '../../../models';
|
||||
import { Brackets, SelectQueryBuilder } from 'typeorm';
|
||||
|
||||
export function generateChannelQuery(q: SelectQueryBuilder<any>, me?: User | null) {
|
||||
if (me == null) {
|
||||
q.andWhere('note.channelId IS NULL');
|
||||
} else {
|
||||
q.leftJoinAndSelect('note.channel', 'channel');
|
||||
|
||||
const channelFollowingQuery = ChannelFollowings.createQueryBuilder('channelFollowing')
|
||||
.select('channelFollowing.followeeId')
|
||||
.where('channelFollowing.followerId = :followerId', { followerId: me.id });
|
||||
|
||||
q.andWhere(new Brackets(qb => { qb
|
||||
// チャンネルのノートではない
|
||||
.where('note.channelId IS NULL')
|
||||
// または自分がフォローしているチャンネルのノート
|
||||
.orWhere(`note.channelId IN (${ channelFollowingQuery.getQuery() })`);
|
||||
}));
|
||||
|
||||
q.setParameters(channelFollowingQuery.getParameters());
|
||||
}
|
||||
}
|
|
@ -35,5 +35,8 @@ export default define(meta, async (ps, me) => {
|
|||
throw new Error('cannot show info of admin');
|
||||
}
|
||||
|
||||
return user;
|
||||
return {
|
||||
...user,
|
||||
token: user.token != null ? '<MASKED>' : user.token,
|
||||
};
|
||||
});
|
||||
|
|
|
@ -418,6 +418,10 @@ export const meta = {
|
|||
|
||||
objectStorageUseProxy: {
|
||||
validator: $.optional.bool
|
||||
},
|
||||
|
||||
objectStorageSetPublicRead: {
|
||||
validator: $.optional.bool
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -673,6 +677,10 @@ export default define(meta, async (ps, me) => {
|
|||
set.objectStorageUseProxy = ps.objectStorageUseProxy;
|
||||
}
|
||||
|
||||
if (ps.objectStorageSetPublicRead !== undefined) {
|
||||
set.objectStorageSetPublicRead = ps.objectStorageSetPublicRead;
|
||||
}
|
||||
|
||||
await getConnection().transaction(async transactionalEntityManager => {
|
||||
const meta = await transactionalEntityManager.findOne(Meta, {
|
||||
order: {
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
import $ from 'cafy';
|
||||
import define from '../../define';
|
||||
import { ApiError } from '../../error';
|
||||
import { Channels, DriveFiles } from '../../../../models';
|
||||
import { Channel } from '../../../../models/entities/channel';
|
||||
import { genId } from '../../../../misc/gen-id';
|
||||
import { ID } from '../../../../misc/cafy-id';
|
||||
|
||||
export const meta = {
|
||||
tags: ['channels'],
|
||||
|
||||
requireCredential: true as const,
|
||||
|
||||
kind: 'write:channels',
|
||||
|
||||
params: {
|
||||
name: {
|
||||
validator: $.str.range(1, 128)
|
||||
},
|
||||
|
||||
description: {
|
||||
validator: $.nullable.optional.str.range(1, 2048)
|
||||
},
|
||||
|
||||
bannerId: {
|
||||
validator: $.nullable.optional.type(ID),
|
||||
}
|
||||
},
|
||||
|
||||
res: {
|
||||
type: 'object' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
ref: 'Channel',
|
||||
},
|
||||
|
||||
errors: {
|
||||
noSuchFile: {
|
||||
message: 'No such file.',
|
||||
code: 'NO_SUCH_FILE',
|
||||
id: 'cd1e9f3e-5a12-4ab4-96f6-5d0a2cc32050'
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
export default define(meta, async (ps, user) => {
|
||||
let banner = null;
|
||||
if (ps.bannerId != null) {
|
||||
banner = await DriveFiles.findOne({
|
||||
id: ps.bannerId,
|
||||
userId: user.id
|
||||
});
|
||||
|
||||
if (banner == null) {
|
||||
throw new ApiError(meta.errors.noSuchFile);
|
||||
}
|
||||
}
|
||||
|
||||
const channel = await Channels.save({
|
||||
id: genId(),
|
||||
createdAt: new Date(),
|
||||
userId: user.id,
|
||||
name: ps.name,
|
||||
description: ps.description || null,
|
||||
bannerId: banner ? banner.id : null,
|
||||
} as Channel);
|
||||
|
||||
return await Channels.pack(channel, user);
|
||||
});
|
|
@ -0,0 +1,28 @@
|
|||
import define from '../../define';
|
||||
import { Channels } from '../../../../models';
|
||||
|
||||
export const meta = {
|
||||
tags: ['channels'],
|
||||
|
||||
requireCredential: false as const,
|
||||
|
||||
res: {
|
||||
type: 'array' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
items: {
|
||||
type: 'object' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
ref: 'Channel',
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export default define(meta, async (ps, me) => {
|
||||
const query = Channels.createQueryBuilder('channel')
|
||||
.where('channel.lastNotedAt IS NOT NULL')
|
||||
.orderBy('channel.lastNotedAt', 'DESC');
|
||||
|
||||
const channels = await query.take(10).getMany();
|
||||
|
||||
return await Promise.all(channels.map(x => Channels.pack(x, me)));
|
||||
});
|
|
@ -0,0 +1,45 @@
|
|||
import $ from 'cafy';
|
||||
import { ID } from '../../../../misc/cafy-id';
|
||||
import define from '../../define';
|
||||
import { ApiError } from '../../error';
|
||||
import { Channels, ChannelFollowings } from '../../../../models';
|
||||
import { genId } from '../../../../misc/gen-id';
|
||||
|
||||
export const meta = {
|
||||
tags: ['channels'],
|
||||
|
||||
requireCredential: true as const,
|
||||
|
||||
kind: 'write:channels',
|
||||
|
||||
params: {
|
||||
channelId: {
|
||||
validator: $.type(ID),
|
||||
},
|
||||
},
|
||||
|
||||
errors: {
|
||||
noSuchChannel: {
|
||||
message: 'No such channel.',
|
||||
code: 'NO_SUCH_CHANNEL',
|
||||
id: 'c0031718-d573-4e85-928e-10039f1fbb68'
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
export default define(meta, async (ps, user) => {
|
||||
const channel = await Channels.findOne({
|
||||
id: ps.channelId,
|
||||
});
|
||||
|
||||
if (channel == null) {
|
||||
throw new ApiError(meta.errors.noSuchChannel);
|
||||
}
|
||||
|
||||
await ChannelFollowings.save({
|
||||
id: genId(),
|
||||
createdAt: new Date(),
|
||||
followerId: user.id,
|
||||
followeeId: channel.id,
|
||||
});
|
||||
});
|
|
@ -0,0 +1,28 @@
|
|||
import define from '../../define';
|
||||
import { Channels, ChannelFollowings } from '../../../../models';
|
||||
|
||||
export const meta = {
|
||||
tags: ['channels', 'account'],
|
||||
|
||||
requireCredential: true as const,
|
||||
|
||||
kind: 'read:channels',
|
||||
|
||||
res: {
|
||||
type: 'array' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
items: {
|
||||
type: 'object' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
ref: 'Channel',
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export default define(meta, async (ps, me) => {
|
||||
const followings = await ChannelFollowings.find({
|
||||
followerId: me.id,
|
||||
});
|
||||
|
||||
return await Promise.all(followings.map(x => Channels.pack(x.followeeId, me)));
|
||||
});
|
|
@ -0,0 +1,28 @@
|
|||
import define from '../../define';
|
||||
import { Channels } from '../../../../models';
|
||||
|
||||
export const meta = {
|
||||
tags: ['channels', 'account'],
|
||||
|
||||
requireCredential: true as const,
|
||||
|
||||
kind: 'read:channels',
|
||||
|
||||
res: {
|
||||
type: 'array' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
items: {
|
||||
type: 'object' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
ref: 'Channel',
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export default define(meta, async (ps, me) => {
|
||||
const channels = await Channels.find({
|
||||
userId: me.id,
|
||||
});
|
||||
|
||||
return await Promise.all(channels.map(x => Channels.pack(x, me)));
|
||||
});
|
|
@ -0,0 +1,43 @@
|
|||
import $ from 'cafy';
|
||||
import { ID } from '../../../../misc/cafy-id';
|
||||
import define from '../../define';
|
||||
import { ApiError } from '../../error';
|
||||
import { Channels } from '../../../../models';
|
||||
|
||||
export const meta = {
|
||||
tags: ['channels'],
|
||||
|
||||
requireCredential: false as const,
|
||||
|
||||
params: {
|
||||
channelId: {
|
||||
validator: $.type(ID),
|
||||
},
|
||||
},
|
||||
|
||||
res: {
|
||||
type: 'object' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
ref: 'Channel',
|
||||
},
|
||||
|
||||
errors: {
|
||||
noSuchChannel: {
|
||||
message: 'No such channel.',
|
||||
code: 'NO_SUCH_CHANNEL',
|
||||
id: '6f6c314b-7486-4897-8966-c04a66a02923'
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
export default define(meta, async (ps, me) => {
|
||||
const channel = await Channels.findOne({
|
||||
id: ps.channelId,
|
||||
});
|
||||
|
||||
if (channel == null) {
|
||||
throw new ApiError(meta.errors.noSuchChannel);
|
||||
}
|
||||
|
||||
return await Channels.pack(channel, me);
|
||||
});
|
|
@ -0,0 +1,99 @@
|
|||
import $ from 'cafy';
|
||||
import { ID } from '../../../../misc/cafy-id';
|
||||
import define from '../../define';
|
||||
import { ApiError } from '../../error';
|
||||
import { Notes, Channels } from '../../../../models';
|
||||
import { makePaginationQuery } from '../../common/make-pagination-query';
|
||||
import { activeUsersChart } from '../../../../services/chart';
|
||||
|
||||
export const meta = {
|
||||
tags: ['notes', 'channels'],
|
||||
|
||||
requireCredential: false as const,
|
||||
|
||||
params: {
|
||||
channelId: {
|
||||
validator: $.type(ID),
|
||||
desc: {
|
||||
'ja-JP': 'チャンネルのID'
|
||||
}
|
||||
},
|
||||
|
||||
limit: {
|
||||
validator: $.optional.num.range(1, 100),
|
||||
default: 10,
|
||||
desc: {
|
||||
'ja-JP': '最大数'
|
||||
}
|
||||
},
|
||||
|
||||
sinceId: {
|
||||
validator: $.optional.type(ID),
|
||||
desc: {
|
||||
'ja-JP': '指定すると、その投稿を基点としてより新しい投稿を取得します'
|
||||
}
|
||||
},
|
||||
|
||||
untilId: {
|
||||
validator: $.optional.type(ID),
|
||||
desc: {
|
||||
'ja-JP': '指定すると、その投稿を基点としてより古い投稿を取得します'
|
||||
}
|
||||
},
|
||||
|
||||
sinceDate: {
|
||||
validator: $.optional.num,
|
||||
desc: {
|
||||
'ja-JP': '指定した時間を基点としてより新しい投稿を取得します。数値は、1970年1月1日 00:00:00 UTC から指定した日時までの経過時間をミリ秒単位で表します。'
|
||||
}
|
||||
},
|
||||
|
||||
untilDate: {
|
||||
validator: $.optional.num,
|
||||
desc: {
|
||||
'ja-JP': '指定した時間を基点としてより古い投稿を取得します。数値は、1970年1月1日 00:00:00 UTC から指定した日時までの経過時間をミリ秒単位で表します。'
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
res: {
|
||||
type: 'array' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
items: {
|
||||
type: 'object' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
ref: 'Note',
|
||||
}
|
||||
},
|
||||
|
||||
errors: {
|
||||
noSuchChannel: {
|
||||
message: 'No such channel.',
|
||||
code: 'NO_SUCH_CHANNEL',
|
||||
id: '4d0eeeba-a02c-4c3c-9966-ef60d38d2e7f'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default define(meta, async (ps, user) => {
|
||||
const channel = await Channels.findOne({
|
||||
id: ps.channelId,
|
||||
});
|
||||
|
||||
if (channel == null) {
|
||||
throw new ApiError(meta.errors.noSuchChannel);
|
||||
}
|
||||
|
||||
//#region Construct query
|
||||
const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId)
|
||||
.andWhere('note.channelId = :channelId', { channelId: channel.id })
|
||||
.leftJoinAndSelect('note.user', 'user')
|
||||
.leftJoinAndSelect('note.channel', 'channel');
|
||||
//#endregion
|
||||
|
||||
const timeline = await query.take(ps.limit!).getMany();
|
||||
|
||||
activeUsersChart.update(user);
|
||||
|
||||
return await Notes.packMany(timeline, user);
|
||||
});
|
|
@ -0,0 +1,42 @@
|
|||
import $ from 'cafy';
|
||||
import { ID } from '../../../../misc/cafy-id';
|
||||
import define from '../../define';
|
||||
import { ApiError } from '../../error';
|
||||
import { Channels, ChannelFollowings } from '../../../../models';
|
||||
|
||||
export const meta = {
|
||||
tags: ['channels'],
|
||||
|
||||
requireCredential: true as const,
|
||||
|
||||
kind: 'write:channels',
|
||||
|
||||
params: {
|
||||
channelId: {
|
||||
validator: $.type(ID),
|
||||
},
|
||||
},
|
||||
|
||||
errors: {
|
||||
noSuchChannel: {
|
||||
message: 'No such channel.',
|
||||
code: 'NO_SUCH_CHANNEL',
|
||||
id: '19959ee9-0153-4c51-bbd9-a98c49dc59d6'
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
export default define(meta, async (ps, user) => {
|
||||
const channel = await Channels.findOne({
|
||||
id: ps.channelId,
|
||||
});
|
||||
|
||||
if (channel == null) {
|
||||
throw new ApiError(meta.errors.noSuchChannel);
|
||||
}
|
||||
|
||||
await ChannelFollowings.delete({
|
||||
followerId: user.id,
|
||||
followeeId: channel.id,
|
||||
});
|
||||
});
|
|
@ -0,0 +1,93 @@
|
|||
import $ from 'cafy';
|
||||
import { ID } from '../../../../misc/cafy-id';
|
||||
import define from '../../define';
|
||||
import { ApiError } from '../../error';
|
||||
import { Channels, DriveFiles } from '../../../../models';
|
||||
|
||||
export const meta = {
|
||||
tags: ['channels'],
|
||||
|
||||
requireCredential: true as const,
|
||||
|
||||
kind: 'write:channels',
|
||||
|
||||
params: {
|
||||
channelId: {
|
||||
validator: $.type(ID),
|
||||
},
|
||||
|
||||
name: {
|
||||
validator: $.optional.str.range(1, 128)
|
||||
},
|
||||
|
||||
description: {
|
||||
validator: $.nullable.optional.str.range(1, 2048)
|
||||
},
|
||||
|
||||
bannerId: {
|
||||
validator: $.nullable.optional.type(ID),
|
||||
}
|
||||
},
|
||||
|
||||
res: {
|
||||
type: 'object' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
ref: 'Channel',
|
||||
},
|
||||
|
||||
errors: {
|
||||
noSuchChannel: {
|
||||
message: 'No such channel.',
|
||||
code: 'NO_SUCH_CHANNEL',
|
||||
id: 'f9c5467f-d492-4c3c-9a8d-a70dacc86512'
|
||||
},
|
||||
|
||||
accessDenied: {
|
||||
message: 'You do not have edit privilege of the channel.',
|
||||
code: 'ACCESS_DENIED',
|
||||
id: '1fb7cb09-d46a-4fdf-b8df-057788cce513'
|
||||
},
|
||||
|
||||
noSuchFile: {
|
||||
message: 'No such file.',
|
||||
code: 'NO_SUCH_FILE',
|
||||
id: 'e86c14a4-0da2-4032-8df3-e737a04c7f3b'
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
export default define(meta, async (ps, me) => {
|
||||
const channel = await Channels.findOne({
|
||||
id: ps.channelId,
|
||||
});
|
||||
|
||||
if (channel == null) {
|
||||
throw new ApiError(meta.errors.noSuchChannel);
|
||||
}
|
||||
|
||||
if (channel.userId !== me.id) {
|
||||
throw new ApiError(meta.errors.accessDenied);
|
||||
}
|
||||
|
||||
let banner = undefined;
|
||||
if (ps.bannerId != null) {
|
||||
banner = await DriveFiles.findOne({
|
||||
id: ps.bannerId,
|
||||
userId: me.id
|
||||
});
|
||||
|
||||
if (banner == null) {
|
||||
throw new ApiError(meta.errors.noSuchFile);
|
||||
}
|
||||
} else if (ps.bannerId === null) {
|
||||
banner = null;
|
||||
}
|
||||
|
||||
await Channels.update(channel.id, {
|
||||
...(ps.name !== undefined ? { name: ps.name } : {}),
|
||||
...(ps.description !== undefined ? { description: ps.description } : {}),
|
||||
...(banner ? { bannerId: banner.id } : {}),
|
||||
});
|
||||
|
||||
return await Channels.pack(channel.id, me);
|
||||
});
|
|
@ -24,7 +24,6 @@ export default define(meta, async (ps, user, token) => {
|
|||
|
||||
return await Users.pack(user, user, {
|
||||
detail: true,
|
||||
includeHasUnreadNotes: true,
|
||||
includeSecrets: isSecure
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
import define from '../../define';
|
||||
import { MutedNotes } from '../../../../models';
|
||||
|
||||
export const meta = {
|
||||
tags: ['account'],
|
||||
|
||||
requireCredential: true as const,
|
||||
|
||||
kind: 'read:account',
|
||||
|
||||
params: {
|
||||
}
|
||||
};
|
||||
|
||||
export default define(meta, async (ps, user) => {
|
||||
return {
|
||||
count: await MutedNotes.count({
|
||||
userId: user.id,
|
||||
reason: 'word'
|
||||
})
|
||||
};
|
||||
});
|
|
@ -195,6 +195,7 @@ export default define(meta, async (ps, me) => {
|
|||
response.objectStorageSecretKey = instance.objectStorageSecretKey;
|
||||
response.objectStorageUseSSL = instance.objectStorageUseSSL;
|
||||
response.objectStorageUseProxy = instance.objectStorageUseProxy;
|
||||
response.objectStorageSetPublicRead = instance.objectStorageSetPublicRead;
|
||||
}
|
||||
|
||||
return response;
|
||||
|
|
|
@ -7,11 +7,12 @@ import { fetchMeta } from '../../../../misc/fetch-meta';
|
|||
import { ApiError } from '../../error';
|
||||
import { ID } from '../../../../misc/cafy-id';
|
||||
import { User } from '../../../../models/entities/user';
|
||||
import { Users, DriveFiles, Notes } from '../../../../models';
|
||||
import { Users, DriveFiles, Notes, Channels } from '../../../../models';
|
||||
import { DriveFile } from '../../../../models/entities/drive-file';
|
||||
import { Note } from '../../../../models/entities/note';
|
||||
import { DB_MAX_NOTE_TEXT_LENGTH } from '../../../../misc/hard-limits';
|
||||
import { noteVisibilities } from '../../../../types';
|
||||
import { Channel } from '../../../../models/entities/channel';
|
||||
|
||||
let maxNoteTextLength = 500;
|
||||
|
||||
|
@ -128,19 +129,26 @@ export const meta = {
|
|||
},
|
||||
|
||||
replyId: {
|
||||
validator: $.optional.type(ID),
|
||||
validator: $.optional.nullable.type(ID),
|
||||
desc: {
|
||||
'ja-JP': '返信対象'
|
||||
}
|
||||
},
|
||||
|
||||
renoteId: {
|
||||
validator: $.optional.type(ID),
|
||||
validator: $.optional.nullable.type(ID),
|
||||
desc: {
|
||||
'ja-JP': 'Renote対象'
|
||||
}
|
||||
},
|
||||
|
||||
channelId: {
|
||||
validator: $.optional.nullable.type(ID),
|
||||
desc: {
|
||||
'ja-JP': 'チャンネル'
|
||||
}
|
||||
},
|
||||
|
||||
poll: {
|
||||
validator: $.optional.obj({
|
||||
choices: $.arr($.str)
|
||||
|
@ -206,7 +214,13 @@ export const meta = {
|
|||
message: 'Poll is already expired.',
|
||||
code: 'CANNOT_CREATE_ALREADY_EXPIRED_POLL',
|
||||
id: '04da457d-b083-4055-9082-955525eda5a5'
|
||||
}
|
||||
},
|
||||
|
||||
noSuchChannel: {
|
||||
message: 'No such channel.',
|
||||
code: 'NO_SUCH_CHANNEL',
|
||||
id: 'b1653923-5453-4edc-b786-7c4f39bb0bbb'
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -269,6 +283,15 @@ export default define(meta, async (ps, user) => {
|
|||
throw new ApiError(meta.errors.contentRequired);
|
||||
}
|
||||
|
||||
let channel: Channel | undefined;
|
||||
if (ps.channelId != null) {
|
||||
channel = await Channels.findOne(ps.channelId);
|
||||
|
||||
if (channel == null) {
|
||||
throw new ApiError(meta.errors.noSuchChannel);
|
||||
}
|
||||
}
|
||||
|
||||
// 投稿を作成
|
||||
const note = await create(user, {
|
||||
createdAt: new Date(),
|
||||
|
@ -286,6 +309,7 @@ export default define(meta, async (ps, user) => {
|
|||
localOnly: ps.localOnly,
|
||||
visibility: ps.visibility,
|
||||
visibleUsers,
|
||||
channel,
|
||||
apMentions: ps.noExtractMentions ? [] : undefined,
|
||||
apHashtags: ps.noExtractHashtags ? [] : undefined,
|
||||
apEmojis: ps.noExtractEmojis ? [] : undefined,
|
||||
|
|
|
@ -80,6 +80,7 @@ export default define(meta, async (ps, user) => {
|
|||
const query = makePaginationQuery(Notes.createQueryBuilder('note'),
|
||||
ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
|
||||
.andWhere('note.visibility = \'public\'')
|
||||
.andWhere('note.channelId IS NULL')
|
||||
.leftJoinAndSelect('note.user', 'user');
|
||||
|
||||
generateRepliesQuery(query, user);
|
||||
|
|
|
@ -13,6 +13,7 @@ import { generateRepliesQuery } from '../../common/generate-replies-query';
|
|||
import { injectPromo } from '../../common/inject-promo';
|
||||
import { injectFeatured } from '../../common/inject-featured';
|
||||
import { generateMutedNoteQuery } from '../../common/generate-muted-note-query';
|
||||
import { generateChannelQuery } from '../../common/generate-channel-query';
|
||||
|
||||
export const meta = {
|
||||
desc: {
|
||||
|
@ -131,6 +132,7 @@ export default define(meta, async (ps, user) => {
|
|||
.leftJoinAndSelect('note.user', 'user')
|
||||
.setParameters(followingQuery.getParameters());
|
||||
|
||||
generateChannelQuery(query, user);
|
||||
generateRepliesQuery(query, user);
|
||||
generateVisibilityQuery(query, user);
|
||||
generateMutedUserQuery(query, user);
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue