Compare commits
24 Commits
Author | SHA1 | Date | |
---|---|---|---|
44d7652171 | |||
c9ed15b682 | |||
8faad646ae | |||
1d50bc3382 | |||
da4af041af | |||
e2ff408f2f | |||
50d1500dfc | |||
94441f93a5 | |||
5f712fbf3c | |||
1c757f10e0 | |||
0508d5f643 | |||
d9986b7a2f | |||
3d79e7a136 | |||
52fb1237ec | |||
8a7197726e | |||
b7f5458684 | |||
52710f3810 | |||
a54de07260 | |||
aa2c8d101e | |||
1441fd93b9 | |||
4a585e8920 | |||
8c4245a09d | |||
e4af16989a | |||
5dc0944fe8 |
25
CHANGELOG.md
25
CHANGELOG.md
@ -1,6 +1,31 @@
|
|||||||
ChangeLog
|
ChangeLog
|
||||||
=========
|
=========
|
||||||
|
|
||||||
|
12.14.0 (2020/02/18)
|
||||||
|
-------------------
|
||||||
|
### ✨Improvements
|
||||||
|
* オブジェクトストレージの設定を実装
|
||||||
|
* サーバーログビューア実装
|
||||||
|
|
||||||
|
12.13.0 (2020/02/18)
|
||||||
|
-------------------
|
||||||
|
### ✨Improvements
|
||||||
|
* プロモーションノート機能を実装
|
||||||
|
* インスタンス管理者が、重要なお知らせやユーザーにやってもらいたいアンケートなどをタイムラインの途中に挿入する機能
|
||||||
|
* プロモーションされる期限を設定できる
|
||||||
|
* 複数のプロモーションがある場合はランダムに選択されて表示される
|
||||||
|
* ユーザーがプロモーションを個別に非表示にすることもできる
|
||||||
|
* ハイライトインジェクション機能を実装
|
||||||
|
* タイムラインの途中におすすめのノートを表示できる機能
|
||||||
|
* 設定で有効/無効を切り替えられる
|
||||||
|
* アクティビティウィジェットを実装
|
||||||
|
* フォトウィジェットを実装
|
||||||
|
* タイムラインの一番上までスクロールできるように
|
||||||
|
* 管理者はモデレーターに変更できないように
|
||||||
|
|
||||||
|
### 🐛Fixes
|
||||||
|
* admin/show-users APIがadminかつmoderator設定されているとき使えない問題を修正
|
||||||
|
|
||||||
12.12.0 (2020/02/17)
|
12.12.0 (2020/02/17)
|
||||||
-------------------
|
-------------------
|
||||||
### ✨Improvements
|
### ✨Improvements
|
||||||
|
@ -412,6 +412,13 @@ dayOverDayChanges: "Daily"
|
|||||||
accessibility: "Accessibility"
|
accessibility: "Accessibility"
|
||||||
clinetSettings: "Client Settings"
|
clinetSettings: "Client Settings"
|
||||||
accountSettings: "Account Settings"
|
accountSettings: "Account Settings"
|
||||||
|
promotion: "Promoted"
|
||||||
|
promote: "Promote"
|
||||||
|
numberOfDays: "Amount of days"
|
||||||
|
hideThisNote: "Hide this note"
|
||||||
|
showFeaturedNotesInTimeline: "Show Featured notes in Timeline"
|
||||||
|
objectStorage: "Object Storage"
|
||||||
|
useObjectStorage: "Use object storage"
|
||||||
_ago:
|
_ago:
|
||||||
unknown: "Unknown"
|
unknown: "Unknown"
|
||||||
future: "Future"
|
future: "Future"
|
||||||
@ -512,6 +519,8 @@ _widgets:
|
|||||||
trends: "Trending"
|
trends: "Trending"
|
||||||
clock: "Clock"
|
clock: "Clock"
|
||||||
rss: "RSS reader"
|
rss: "RSS reader"
|
||||||
|
activity: "Activity"
|
||||||
|
photos: "Photos"
|
||||||
_cw:
|
_cw:
|
||||||
hide: "Hide"
|
hide: "Hide"
|
||||||
show: "Load more"
|
show: "Load more"
|
||||||
|
@ -412,6 +412,13 @@ dayOverDayChanges: "Dif diaria"
|
|||||||
accessibility: "Accesibilidad"
|
accessibility: "Accesibilidad"
|
||||||
clinetSettings: "Ajustes del cliente"
|
clinetSettings: "Ajustes del cliente"
|
||||||
accountSettings: "Ajustes de cuenta"
|
accountSettings: "Ajustes de cuenta"
|
||||||
|
promotion: "Promovido"
|
||||||
|
promote: "Promover"
|
||||||
|
numberOfDays: "Cantidad de dias"
|
||||||
|
hideThisNote: "Ocultar esta nota"
|
||||||
|
showFeaturedNotesInTimeline: "Mostrar notas destacadas en la línea de tiempo"
|
||||||
|
objectStorage: "Almacenamiento de objetos"
|
||||||
|
useObjectStorage: "Usar almacenamiento de objetos"
|
||||||
_ago:
|
_ago:
|
||||||
unknown: "Desconocido"
|
unknown: "Desconocido"
|
||||||
future: "Futuro"
|
future: "Futuro"
|
||||||
@ -512,6 +519,8 @@ _widgets:
|
|||||||
trends: "Tendencias"
|
trends: "Tendencias"
|
||||||
clock: "Reloj"
|
clock: "Reloj"
|
||||||
rss: "Lector RSS"
|
rss: "Lector RSS"
|
||||||
|
activity: "Actividad"
|
||||||
|
photos: "Fotos"
|
||||||
_cw:
|
_cw:
|
||||||
hide: "Ocultar"
|
hide: "Ocultar"
|
||||||
show: "Ver más"
|
show: "Ver más"
|
||||||
|
@ -412,6 +412,13 @@ dayOverDayChanges: "Diff quotidien"
|
|||||||
accessibility: "Accessibilité"
|
accessibility: "Accessibilité"
|
||||||
clinetSettings: "Paramètres du client"
|
clinetSettings: "Paramètres du client"
|
||||||
accountSettings: "Paramètres du compte"
|
accountSettings: "Paramètres du compte"
|
||||||
|
promotion: "Promu"
|
||||||
|
promote: "Promouvoir"
|
||||||
|
numberOfDays: "Nombre de jours"
|
||||||
|
hideThisNote: "Masquer cette note"
|
||||||
|
showFeaturedNotesInTimeline: "Afficher les notes en vedette dans Fil d'actualité"
|
||||||
|
objectStorage: "Stockage d'objets"
|
||||||
|
useObjectStorage: "Utiliser le stockage d'objets"
|
||||||
_ago:
|
_ago:
|
||||||
unknown: "Inconnu"
|
unknown: "Inconnu"
|
||||||
future: "Futur"
|
future: "Futur"
|
||||||
@ -492,6 +499,7 @@ _widgets:
|
|||||||
trends: "Tendances"
|
trends: "Tendances"
|
||||||
clock: "Horloge"
|
clock: "Horloge"
|
||||||
rss: "Lecteur de flux RSS"
|
rss: "Lecteur de flux RSS"
|
||||||
|
activity: "Activités"
|
||||||
_cw:
|
_cw:
|
||||||
hide: "Masquer"
|
hide: "Masquer"
|
||||||
show: "Voir plus"
|
show: "Voir plus"
|
||||||
|
@ -412,6 +412,15 @@ dayOverDayChanges: "前日比"
|
|||||||
accessibility: "アクセシビリティ"
|
accessibility: "アクセシビリティ"
|
||||||
clinetSettings: "クライアント設定"
|
clinetSettings: "クライアント設定"
|
||||||
accountSettings: "アカウント設定"
|
accountSettings: "アカウント設定"
|
||||||
|
promotion: "プロモーション"
|
||||||
|
promote: "プロモート"
|
||||||
|
numberOfDays: "日数"
|
||||||
|
hideThisNote: "このノートを非表示"
|
||||||
|
showFeaturedNotesInTimeline: "タイムラインにおすすめのノートを表示する"
|
||||||
|
objectStorage: "オブジェクトストレージ"
|
||||||
|
useObjectStorage: "オブジェクトストレージを使用"
|
||||||
|
serverLogs: "サーバーログ"
|
||||||
|
deleteAll: "全て削除"
|
||||||
|
|
||||||
_ago:
|
_ago:
|
||||||
unknown: "謎"
|
unknown: "謎"
|
||||||
@ -521,6 +530,8 @@ _widgets:
|
|||||||
trends: "トレンド"
|
trends: "トレンド"
|
||||||
clock: "時計"
|
clock: "時計"
|
||||||
rss: "RSSリーダー"
|
rss: "RSSリーダー"
|
||||||
|
activity: "アクティビティ"
|
||||||
|
photos: "フォト"
|
||||||
|
|
||||||
_cw:
|
_cw:
|
||||||
hide: "隠す"
|
hide: "隠す"
|
||||||
|
@ -1,2 +1,34 @@
|
|||||||
---
|
---
|
||||||
_lang_: "ಕನ್ನಡ"
|
_lang_: "ಕನ್ನಡ"
|
||||||
|
introMisskey: "ಸ್ವಾಗತ! Misskey ಓಪನ್ ಸೋರ್ಸ್ ಒಕ್ಕೂಟ ಮೈಕ್ರೋಬ್ಲಾಗಿಂಗ್ ಸೇವೆಯಾಗಿದೆ.\n ಏನಾಗುತ್ತಿದೆ ಎಂಬುದನ್ನು ಹಂಚಿಕೊಳ್ಳಲು ಅಥವಾ ನಿಮ್ಮ ಬಗ್ಗೆ ಎಲ್ಲರಿಗೂ ಹೇಳಲು \"ಟಿಪ್ಪಣಿ\"ಗಳನ್ನು ರಚಿಸಿ📡\n \"ಸ್ಪಂದನೆ\" ಕ್ರಿಯೆಯೊಂದಿಗೆ, ನೀವು ಎಲ್ಲರ ಟಿಪ್ಪಣಿಗಳಿಗೆ ತ್ವರಿತವಾಗಿ ಸ್ಪಂದನೆಗಳನ್ನು ಕೂಡ ಸೇರಿಸಬಹುದು.👍\n ಹೊಸ ಜಗತ್ತನ್ನು ಅನ್ವೇಷಿಸಿ🚀"
|
||||||
|
monthAndDay: "{month}ನೇ ತಿಂಗಳ {day}ನೇ ದಿನ"
|
||||||
|
search: "ಹುಡುಕು"
|
||||||
|
notifications: "ಅಧಿಸೂಚನೆಗಳು"
|
||||||
|
username: "ಬಳಕೆಹೆಸರು"
|
||||||
|
password: "ಗುಪ್ತಪದ"
|
||||||
|
fetchingAsApObject: "ಒಕ್ಕೂಟದಿಂದ ಪಡೆಯಲಾಗುತ್ತಿದೆ..."
|
||||||
|
ok: "ಸರಿ"
|
||||||
|
gotIt: "ಅರ್ಥವಾಯಿತು!"
|
||||||
|
cancel: "ರದ್ದು"
|
||||||
|
enterUsername: "ಬಳಕೆಹೆಸರನ್ನು ಭರ್ತಿ ಮಾಡಿ"
|
||||||
|
renotedBy: "{user} ಪುನರಾವರ್ತಿಸಿದರು"
|
||||||
|
noNotes: "ಟಿಪ್ಪಣಿಗಳಿಲ್ಲ"
|
||||||
|
noNotifications: "ಅಧಿಸೂಚನೆಗಳಿಲ್ಲ"
|
||||||
|
instance: "ನಿದರ್ಶನ"
|
||||||
|
settings: "ಸಿದ್ಧತೆಗಳು"
|
||||||
|
profile: "ಪ್ರೊಫೈಲು"
|
||||||
|
timeline: "ಸಮಯಸಾಲು"
|
||||||
|
noAccountDescription: "ಇವರು ಸ್ವಯಂ ಪರಿಚಯ ರಚಿಸಿಲ್ಲ"
|
||||||
|
login: "ಪ್ರವೇಶ"
|
||||||
|
loggingIn: "ಪ್ರವೇಶಿಸುತ್ತಾ..."
|
||||||
|
logout: "ಆಚೆಗೆ"
|
||||||
|
signup: "ನೋಂದಣಿ"
|
||||||
|
uploading: "ಅಪ್ಲೋಡಾಗುತ್ತಿದೆ"
|
||||||
|
save: "ಉಳಿಸಿ"
|
||||||
|
users: "ಬಳಕೆದಾರ"
|
||||||
|
instances: "ನಿದರ್ಶನ"
|
||||||
|
_widgets:
|
||||||
|
notifications: "ಅಧಿಸೂಚನೆಗಳು"
|
||||||
|
timeline: "ಸಮಯಸಾಲು"
|
||||||
|
_profile:
|
||||||
|
username: "ಬಳಕೆಹೆಸರು"
|
||||||
|
@ -412,6 +412,13 @@ dayOverDayChanges: "어제보다"
|
|||||||
accessibility: "접근성"
|
accessibility: "접근성"
|
||||||
clinetSettings: "클라이언트 설정"
|
clinetSettings: "클라이언트 설정"
|
||||||
accountSettings: "계정 설정"
|
accountSettings: "계정 설정"
|
||||||
|
promotion: "프로모션"
|
||||||
|
promote: "프로모션하기"
|
||||||
|
numberOfDays: "며칠동안"
|
||||||
|
hideThisNote: "이 노트를 숨기기"
|
||||||
|
showFeaturedNotesInTimeline: "타임라인에 추천 노트를 표시"
|
||||||
|
objectStorage: "오브젝트 스토리지"
|
||||||
|
useObjectStorage: "오브젝트 스토리지를 사용"
|
||||||
_ago:
|
_ago:
|
||||||
unknown: "알 수 없음"
|
unknown: "알 수 없음"
|
||||||
future: "미래"
|
future: "미래"
|
||||||
@ -512,6 +519,8 @@ _widgets:
|
|||||||
trends: "트렌드"
|
trends: "트렌드"
|
||||||
clock: "시계"
|
clock: "시계"
|
||||||
rss: "RSS 리더"
|
rss: "RSS 리더"
|
||||||
|
activity: "활동"
|
||||||
|
photos: "사진"
|
||||||
_cw:
|
_cw:
|
||||||
hide: "숨기기"
|
hide: "숨기기"
|
||||||
show: "더 보기"
|
show: "더 보기"
|
||||||
|
@ -414,6 +414,16 @@ _time:
|
|||||||
_tutorial:
|
_tutorial:
|
||||||
title: "Misskey的使用方法"
|
title: "Misskey的使用方法"
|
||||||
step1_1: "欢迎!"
|
step1_1: "欢迎!"
|
||||||
|
step1_2: "这个页面叫做「时间线」,它会按照时间顺序显示所有你「关注」的人所发的「帖子」。"
|
||||||
|
step1_3: "如果你并没有发布任何帖子,也没有关注其他的人,你的时间线页面应当什么都没有显示。"
|
||||||
|
step2_1: "在你想发布一些帖子之前,让我们先进行一下个人资料设置。"
|
||||||
|
step2_2: "如果别人能够更加的了解你,关注你的概率也会得到提升。"
|
||||||
|
step3_1: "已经设置完个人资料了吗?"
|
||||||
|
step3_2: "那么接下来,试着写一些什么东西来发布吧。你可以通过点击屏幕上的铅笔图标来打开投稿页面。"
|
||||||
|
step3_3: "写完内容后,点击窗口右上方的按钮就可以投稿。"
|
||||||
|
step3_4: "不知道说些什么好吗?那就写下「Misskey我来啦!」这样的话吧。"
|
||||||
|
step4_1: "将你的话语发布出去了吗?"
|
||||||
|
step4_2: "太棒了!现在你可以在你的时间线中看到你刚刚发布的帖子了。"
|
||||||
step7_3: "接下来,享受Misskey带来的乐趣吧🚀"
|
step7_3: "接下来,享受Misskey带来的乐趣吧🚀"
|
||||||
_2fa:
|
_2fa:
|
||||||
alreadyRegistered: "此设备已被注册"
|
alreadyRegistered: "此设备已被注册"
|
||||||
@ -463,6 +473,7 @@ _widgets:
|
|||||||
trends: "趋势"
|
trends: "趋势"
|
||||||
clock: "时钟"
|
clock: "时钟"
|
||||||
rss: "RSS阅读器"
|
rss: "RSS阅读器"
|
||||||
|
activity: "活动"
|
||||||
_cw:
|
_cw:
|
||||||
hide: "隐藏"
|
hide: "隐藏"
|
||||||
show: "查看更多"
|
show: "查看更多"
|
||||||
|
28
migration/1581979837262-promo.ts
Normal file
28
migration/1581979837262-promo.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import {MigrationInterface, QueryRunner} from "typeorm";
|
||||||
|
|
||||||
|
export class promo1581979837262 implements MigrationInterface {
|
||||||
|
name = 'promo1581979837262'
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<any> {
|
||||||
|
await queryRunner.query(`CREATE TABLE "promo_note" ("noteId" character varying(32) NOT NULL, "expiresAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, CONSTRAINT "REL_e263909ca4fe5d57f8d4230dd5" UNIQUE ("noteId"), CONSTRAINT "PK_e263909ca4fe5d57f8d4230dd5c" PRIMARY KEY ("noteId"))`, undefined);
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_83f0862e9bae44af52ced7099e" ON "promo_note" ("userId") `, undefined);
|
||||||
|
await queryRunner.query(`CREATE TABLE "promo_read" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "noteId" character varying(32) NOT NULL, CONSTRAINT "PK_61917c1541002422b703318b7c9" PRIMARY KEY ("id"))`, undefined);
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_9657d55550c3d37bfafaf7d4b0" ON "promo_read" ("userId") `, undefined);
|
||||||
|
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_2882b8a1a07c7d281a98b6db16" ON "promo_read" ("userId", "noteId") `, undefined);
|
||||||
|
await queryRunner.query(`ALTER TABLE "promo_note" ADD CONSTRAINT "FK_e263909ca4fe5d57f8d4230dd5c" FOREIGN KEY ("noteId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined);
|
||||||
|
await queryRunner.query(`ALTER TABLE "promo_read" ADD CONSTRAINT "FK_9657d55550c3d37bfafaf7d4b05" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined);
|
||||||
|
await queryRunner.query(`ALTER TABLE "promo_read" ADD CONSTRAINT "FK_a46a1a603ecee695d7db26da5f4" FOREIGN KEY ("noteId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<any> {
|
||||||
|
await queryRunner.query(`ALTER TABLE "promo_read" DROP CONSTRAINT "FK_a46a1a603ecee695d7db26da5f4"`, undefined);
|
||||||
|
await queryRunner.query(`ALTER TABLE "promo_read" DROP CONSTRAINT "FK_9657d55550c3d37bfafaf7d4b05"`, undefined);
|
||||||
|
await queryRunner.query(`ALTER TABLE "promo_note" DROP CONSTRAINT "FK_e263909ca4fe5d57f8d4230dd5c"`, undefined);
|
||||||
|
await queryRunner.query(`DROP INDEX "IDX_2882b8a1a07c7d281a98b6db16"`, undefined);
|
||||||
|
await queryRunner.query(`DROP INDEX "IDX_9657d55550c3d37bfafaf7d4b0"`, undefined);
|
||||||
|
await queryRunner.query(`DROP TABLE "promo_read"`, undefined);
|
||||||
|
await queryRunner.query(`DROP INDEX "IDX_83f0862e9bae44af52ced7099e"`, undefined);
|
||||||
|
await queryRunner.query(`DROP TABLE "promo_note"`, undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
14
migration/1582019042083-featured-injecttion.ts
Normal file
14
migration/1582019042083-featured-injecttion.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import {MigrationInterface, QueryRunner} from "typeorm";
|
||||||
|
|
||||||
|
export class featuredInjecttion1582019042083 implements MigrationInterface {
|
||||||
|
name = 'featuredInjecttion1582019042083'
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<any> {
|
||||||
|
await queryRunner.query(`ALTER TABLE "user_profile" ADD "injectFeaturedNote" boolean NOT NULL DEFAULT true`, undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<any> {
|
||||||
|
await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "injectFeaturedNote"`, undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "misskey",
|
"name": "misskey",
|
||||||
"author": "syuilo <syuilotan@yahoo.co.jp>",
|
"author": "syuilo <syuilotan@yahoo.co.jp>",
|
||||||
"version": "12.12.0",
|
"version": "12.14.0",
|
||||||
"codename": "indigo",
|
"codename": "indigo",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
@ -44,11 +44,11 @@
|
|||||||
<mk-avatar :user="$store.state.i" class="avatar"/><mk-acct class="text" :user="$store.state.i"/>
|
<mk-avatar :user="$store.state.i" class="avatar"/><mk-acct class="text" :user="$store.state.i"/>
|
||||||
</button>
|
</button>
|
||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
<router-link class="item index" active-class="active" to="/" exact v-if="$store.getters.isSignedIn">
|
<button class="item _button index active" @click="top()" v-if="$route.name === 'index'">
|
||||||
<fa :icon="faHome" fixed-width/><span class="text">{{ $t('timeline') }}</span>
|
<fa :icon="faHome" fixed-width/><span class="text">{{ $store.getters.isSignedIn ? $t('timeline') : $t('home') }}</span>
|
||||||
</router-link>
|
</button>
|
||||||
<router-link class="item index" active-class="active" to="/" exact v-else>
|
<router-link class="item index" active-class="active" to="/" exact v-else>
|
||||||
<fa :icon="faHome" fixed-width/><span class="text">{{ $t('home') }}</span>
|
<fa :icon="faHome" fixed-width/><span class="text">{{ $store.getters.isSignedIn ? $t('timeline') : $t('home') }}</span>
|
||||||
</router-link>
|
</router-link>
|
||||||
<button class="item _button notifications" @click="notificationsOpen = !notificationsOpen" ref="notificationButton" v-if="$store.getters.isSignedIn">
|
<button class="item _button notifications" @click="notificationsOpen = !notificationsOpen" ref="notificationButton" v-if="$store.getters.isSignedIn">
|
||||||
<fa :icon="faBell" fixed-width/><span class="text">{{ $t('notifications') }}</span>
|
<fa :icon="faBell" fixed-width/><span class="text">{{ $t('notifications') }}</span>
|
||||||
@ -140,8 +140,9 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
<button v-if="$store.getters.isSignedIn" class="button nav _button" @click="showNav = true" ref="navButton"><fa :icon="faBars"/><i v-if="$store.state.i.hasUnreadSpecifiedNotes || $store.state.i.hasPendingReceivedFollowRequest || $store.state.i.hasUnreadMessagingMessage || $store.state.i.hasUnreadAnnouncement"><fa :icon="faCircle"/></i></button>
|
<button class="button nav _button" @click="showNav = true" ref="navButton"><fa :icon="faBars"/><i v-if="$store.getters.isSignedIn && ($store.state.i.hasUnreadSpecifiedNotes || $store.state.i.hasPendingReceivedFollowRequest || $store.state.i.hasUnreadMessagingMessage || $store.state.i.hasUnreadAnnouncement)"><fa :icon="faCircle"/></i></button>
|
||||||
<button v-if="$store.getters.isSignedIn" class="button home _button" :disabled="$route.path === '/'" @click="$router.push('/')"><fa :icon="faHome"/></button>
|
<button v-if="$route.name === 'index'" class="button home _button" @click="top()"><fa :icon="faHome"/></button>
|
||||||
|
<button v-else class="button home _button" @click="$router.push('/')"><fa :icon="faHome"/></button>
|
||||||
<button v-if="$store.getters.isSignedIn" class="button notifications _button" @click="notificationsOpen = !notificationsOpen" ref="notificationButton2"><fa :icon="notificationsOpen ? faTimes : faBell"/><i v-if="$store.state.i.hasUnreadNotification"><fa :icon="faCircle"/></i></button>
|
<button v-if="$store.getters.isSignedIn" class="button notifications _button" @click="notificationsOpen = !notificationsOpen" ref="notificationButton2"><fa :icon="notificationsOpen ? faTimes : faBell"/><i v-if="$store.state.i.hasUnreadNotification"><fa :icon="faCircle"/></i></button>
|
||||||
<button v-if="$store.getters.isSignedIn" class="button post _buttonPrimary" @click="post()"><fa :icon="faPencilAlt"/></button>
|
<button v-if="$store.getters.isSignedIn" class="button post _buttonPrimary" @click="post()"><fa :icon="faPencilAlt"/></button>
|
||||||
</div>
|
</div>
|
||||||
@ -321,6 +322,10 @@ export default Vue.extend({
|
|||||||
setTimeout(adjust, 100);
|
setTimeout(adjust, 100);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
top() {
|
||||||
|
window.scroll({ top: 0, behavior: 'smooth' });
|
||||||
|
},
|
||||||
|
|
||||||
help() {
|
help() {
|
||||||
this.$router.push('/docs/keyboard-shortcut');
|
this.$router.push('/docs/keyboard-shortcut');
|
||||||
},
|
},
|
||||||
@ -601,7 +606,9 @@ export default Vue.extend({
|
|||||||
'calendar',
|
'calendar',
|
||||||
'rss',
|
'rss',
|
||||||
'trends',
|
'trends',
|
||||||
'clock'
|
'clock',
|
||||||
|
'activity',
|
||||||
|
'photos',
|
||||||
];
|
];
|
||||||
|
|
||||||
this.$root.menu({
|
this.$root.menu({
|
||||||
|
BIN
src/client/assets/fedi.jpg
Normal file
BIN
src/client/assets/fedi.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 76 KiB |
@ -2,7 +2,7 @@
|
|||||||
<sequential-entrance class="sqadhkmv" ref="list" :direction="direction" :reversed="reversed">
|
<sequential-entrance class="sqadhkmv" ref="list" :direction="direction" :reversed="reversed">
|
||||||
<template v-for="(item, i) in items">
|
<template v-for="(item, i) in items">
|
||||||
<slot :item="item" :i="i"></slot>
|
<slot :item="item" :i="i"></slot>
|
||||||
<div class="separator" :key="item.id + '_date'" v-if="i != items.length - 1 && new Date(item.createdAt).getDate() != new Date(items[i + 1].createdAt).getDate()">
|
<div class="separator" :key="item.id + '_date'" v-if="showDate(i, item)">
|
||||||
<p class="date">
|
<p class="date">
|
||||||
<span><fa class="icon" :icon="faAngleUp"/>{{ getDateText(item.createdAt) }}</span>
|
<span><fa class="icon" :icon="faAngleUp"/>{{ getDateText(item.createdAt) }}</span>
|
||||||
<span>{{ getDateText(items[i + 1].createdAt) }}<fa class="icon" :icon="faAngleDown"/></span>
|
<span>{{ getDateText(items[i + 1].createdAt) }}<fa class="icon" :icon="faAngleDown"/></span>
|
||||||
@ -52,6 +52,16 @@ export default Vue.extend({
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
showDate(i, item) {
|
||||||
|
return (
|
||||||
|
i != this.items.length - 1 &&
|
||||||
|
new Date(item.createdAt).getDate() != new Date(this.items[i + 1].createdAt).getDate() &&
|
||||||
|
!item._prId_ &&
|
||||||
|
!this.items[i + 1]._prId_ &&
|
||||||
|
!item._featuredId_ &&
|
||||||
|
!this.items[i + 1]._featuredId_);
|
||||||
|
},
|
||||||
|
|
||||||
focus() {
|
focus() {
|
||||||
this.$refs.list.focus();
|
this.$refs.list.focus();
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,9 @@
|
|||||||
>
|
>
|
||||||
<x-sub v-for="note in conversation" :key="note.id" :note="note"/>
|
<x-sub v-for="note in conversation" :key="note.id" :note="note"/>
|
||||||
<x-sub :note="appearNote.reply" class="reply-to" v-if="appearNote.reply"/>
|
<x-sub :note="appearNote.reply" class="reply-to" v-if="appearNote.reply"/>
|
||||||
<div class="pinned" v-if="pinned"><fa :icon="faThumbtack"/> {{ $t('pinnedNote') }}</div>
|
<div class="info" v-if="pinned"><fa :icon="faThumbtack"/> {{ $t('pinnedNote') }}</div>
|
||||||
|
<div class="info" v-if="appearNote._prId_"><fa :icon="faBullhorn"/> {{ $t('promotion') }}<button class="_textButton hide" @click="readPromo()">{{ $t('hideThisNote') }} <fa :icon="faTimes"/></button></div>
|
||||||
|
<div class="info" v-if="appearNote._featuredId_"><fa :icon="faBolt"/> {{ $t('featured') }}</div>
|
||||||
<div class="renote" v-if="isRenote">
|
<div class="renote" v-if="isRenote">
|
||||||
<mk-avatar class="avatar" :user="note.user"/>
|
<mk-avatar class="avatar" :user="note.user"/>
|
||||||
<fa :icon="faRetweet"/>
|
<fa :icon="faRetweet"/>
|
||||||
@ -83,7 +85,7 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import { faStar, faLink, faExternalLinkSquareAlt, faPlus, faMinus, faRetweet, faReply, faReplyAll, faEllipsisH, faHome, faUnlock, faEnvelope, faThumbtack, faBan, faQuoteRight } from '@fortawesome/free-solid-svg-icons';
|
import { faBolt, faTimes, faBullhorn, faStar, faLink, faExternalLinkSquareAlt, faPlus, faMinus, faRetweet, faReply, faReplyAll, faEllipsisH, faHome, faUnlock, faEnvelope, faThumbtack, faBan, faQuoteRight } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { faCopy, faTrashAlt, faEye, faEyeSlash } from '@fortawesome/free-regular-svg-icons';
|
import { faCopy, faTrashAlt, faEye, faEyeSlash } from '@fortawesome/free-regular-svg-icons';
|
||||||
import { parse } from '../../mfm/parse';
|
import { parse } from '../../mfm/parse';
|
||||||
import { sum, unique } from '../../prelude/array';
|
import { sum, unique } from '../../prelude/array';
|
||||||
@ -140,7 +142,7 @@ export default Vue.extend({
|
|||||||
replies: [],
|
replies: [],
|
||||||
showContent: false,
|
showContent: false,
|
||||||
hideThisNote: false,
|
hideThisNote: false,
|
||||||
faPlus, faMinus, faRetweet, faReply, faReplyAll, faEllipsisH, faHome, faUnlock, faEnvelope, faThumbtack, faBan
|
faBolt, faTimes, faBullhorn, faPlus, faMinus, faRetweet, faReply, faReplyAll, faEllipsisH, faHome, faUnlock, faEnvelope, faThumbtack, faBan
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -263,6 +265,13 @@ export default Vue.extend({
|
|||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
readPromo() {
|
||||||
|
(this as any).$root.api('promo/read', {
|
||||||
|
noteId: this.appearNote.id
|
||||||
|
});
|
||||||
|
this.hideThisNote = true;
|
||||||
|
},
|
||||||
|
|
||||||
capture(withHandler = false) {
|
capture(withHandler = false) {
|
||||||
if (this.$store.getters.isSignedIn) {
|
if (this.$store.getters.isSignedIn) {
|
||||||
this.connection.send(document.body.contains(this.$el) ? 'sn' : 's', { id: this.appearNote.id });
|
this.connection.send(document.body.contains(this.$el) ? 'sn' : 's', { id: this.appearNote.id });
|
||||||
@ -522,6 +531,15 @@ export default Vue.extend({
|
|||||||
text: this.$t('pin'),
|
text: this.$t('pin'),
|
||||||
action: () => this.togglePin(true)
|
action: () => this.togglePin(true)
|
||||||
} : undefined,
|
} : undefined,
|
||||||
|
...(this.$store.state.i.isModerator || this.$store.state.i.isAdmin ? [
|
||||||
|
null,
|
||||||
|
{
|
||||||
|
icon: faBullhorn,
|
||||||
|
text: this.$t('promote'),
|
||||||
|
action: this.promote
|
||||||
|
}]
|
||||||
|
: []
|
||||||
|
),
|
||||||
...(this.appearNote.userId == this.$store.state.i.id ? [
|
...(this.appearNote.userId == this.$store.state.i.id ? [
|
||||||
null,
|
null,
|
||||||
{
|
{
|
||||||
@ -614,6 +632,30 @@ export default Vue.extend({
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async promote() {
|
||||||
|
const { canceled, result: days } = await this.$root.dialog({
|
||||||
|
title: this.$t('numberOfDays'),
|
||||||
|
input: { type: 'number' }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (canceled) return;
|
||||||
|
|
||||||
|
this.$root.api('admin/promo/create', {
|
||||||
|
noteId: this.appearNote.id,
|
||||||
|
expiresAt: Date.now() + (86400000 * days)
|
||||||
|
}).then(() => {
|
||||||
|
this.$root.dialog({
|
||||||
|
type: 'success',
|
||||||
|
iconOnly: true, autoClose: true
|
||||||
|
});
|
||||||
|
}).catch(e => {
|
||||||
|
this.$root.dialog({
|
||||||
|
type: 'error',
|
||||||
|
text: e
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
focus() {
|
focus() {
|
||||||
this.$el.focus();
|
this.$el.focus();
|
||||||
},
|
},
|
||||||
@ -710,7 +752,9 @@ export default Vue.extend({
|
|||||||
border-radius: 0 0 var(--radius) var(--radius);
|
border-radius: 0 0 var(--radius) var(--radius);
|
||||||
}
|
}
|
||||||
|
|
||||||
> .pinned {
|
> .info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
padding: 16px 32px 8px 32px;
|
padding: 16px 32px 8px 32px;
|
||||||
line-height: 24px;
|
line-height: 24px;
|
||||||
font-size: 90%;
|
font-size: 90%;
|
||||||
@ -724,9 +768,14 @@ export default Vue.extend({
|
|||||||
> [data-icon] {
|
> [data-icon] {
|
||||||
margin-right: 4px;
|
margin-right: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
> .hide {
|
||||||
|
margin-left: auto;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
> .pinned + .article {
|
> .info + .article {
|
||||||
padding-top: 8px;
|
padding-top: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<x-list ref="notes" class="notes" :items="notes" v-slot="{ item: note }" :direction="reversed ? 'up' : 'down'" :reversed="reversed">
|
<x-list ref="notes" class="notes" :items="notes" v-slot="{ item: note }" :direction="reversed ? 'up' : 'down'" :reversed="reversed">
|
||||||
<x-note :note="note" :detail="detail" :key="note.id"/>
|
<x-note :note="note" :detail="detail" :key="note._featuredId_ || note._prId_ || note.id"/>
|
||||||
</x-list>
|
</x-list>
|
||||||
|
|
||||||
<div class="more" v-if="more && !reversed" style="margin-top: var(--margin);">
|
<div class="more" v-if="more && !reversed" style="margin-top: var(--margin);">
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
<template #header><mk-user-name :user="user"/></template>
|
<template #header><mk-user-name :user="user"/></template>
|
||||||
<div class="vrcsvlkm">
|
<div class="vrcsvlkm">
|
||||||
<mk-button @click="resetPassword()" primary>{{ $t('resetPassword') }}</mk-button>
|
<mk-button @click="resetPassword()" primary>{{ $t('resetPassword') }}</mk-button>
|
||||||
<mk-switch v-if="$store.state.i.isAdmin" @change="toggleModerator()" v-model="moderator">{{ $t('moderator') }}</mk-switch>
|
<mk-switch v-if="$store.state.i.isAdmin && !user.isAdmin" @change="toggleModerator()" v-model="moderator">{{ $t('moderator') }}</mk-switch>
|
||||||
<mk-switch @change="toggleSilence()" v-model="silenced">{{ $t('silence') }}</mk-switch>
|
<mk-switch @change="toggleSilence()" v-model="silenced">{{ $t('silence') }}</mk-switch>
|
||||||
<mk-switch @change="toggleSuspend()" v-model="suspended">{{ $t('suspend') }}</mk-switch>
|
<mk-switch @change="toggleSuspend()" v-model="suspended">{{ $t('suspend') }}</mk-switch>
|
||||||
</div>
|
</div>
|
||||||
@ -47,7 +47,7 @@ export default Vue.extend({
|
|||||||
type: 'waiting',
|
type: 'waiting',
|
||||||
iconOnly: true
|
iconOnly: true
|
||||||
});
|
});
|
||||||
|
|
||||||
this.$root.api('admin/reset-password', {
|
this.$root.api('admin/reset-password', {
|
||||||
userId: this.user.id,
|
userId: this.user.id,
|
||||||
}).then(({ password }) => {
|
}).then(({ password }) => {
|
||||||
|
@ -5,6 +5,38 @@
|
|||||||
|
|
||||||
<mk-instance-stats style="margin-bottom: var(--margin);"/>
|
<mk-instance-stats style="margin-bottom: var(--margin);"/>
|
||||||
|
|
||||||
|
<section class="_card logs">
|
||||||
|
<div class="_title"><fa :icon="faStream"/> {{ $t('serverLogs') }}</div>
|
||||||
|
<div class="_content">
|
||||||
|
<div class="_inputs">
|
||||||
|
<mk-input v-model="logDomain" :debounce="true">
|
||||||
|
<span>{{ $t('domain') }}</span>
|
||||||
|
</mk-input>
|
||||||
|
<mk-select v-model="logLevel">
|
||||||
|
<template #label>{{ $t('level') }}</template>
|
||||||
|
<option value="all">{{ $t('levels.all') }}</option>
|
||||||
|
<option value="info">{{ $t('levels.info') }}</option>
|
||||||
|
<option value="success">{{ $t('levels.success') }}</option>
|
||||||
|
<option value="warning">{{ $t('levels.warning') }}</option>
|
||||||
|
<option value="error">{{ $t('levels.error') }}</option>
|
||||||
|
<option value="debug">{{ $t('levels.debug') }}</option>
|
||||||
|
</mk-select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="logs">
|
||||||
|
<code v-for="log in logs" :key="log.id" :class="log.level">
|
||||||
|
<details>
|
||||||
|
<summary><mk-time :time="log.createdAt"/> [{{ log.domain.join('.') }}] {{ log.message }}</summary>
|
||||||
|
<vue-json-pretty v-if="log.data" :data="log.data"></vue-json-pretty>
|
||||||
|
</details>
|
||||||
|
</code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="_footer">
|
||||||
|
<mk-button @click="deleteAllLogs()" primary><fa :icon="faTrashAlt"/> {{ $t('deleteAll') }}</mk-button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section class="_card chart">
|
<section class="_card chart">
|
||||||
<div class="_title"><fa :icon="faMicrochip"/> {{ $t('cpuAndMemory') }}</div>
|
<div class="_title"><fa :icon="faMicrochip"/> {{ $t('cpuAndMemory') }}</div>
|
||||||
<div class="_content" style="margin-top: -8px; margin-bottom: -12px;">
|
<div class="_content" style="margin-top: -8px; margin-bottom: -12px;">
|
||||||
@ -67,9 +99,13 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import { faServer, faExchangeAlt, faMicrochip, faHdd } from '@fortawesome/free-solid-svg-icons';
|
import { faServer, faExchangeAlt, faMicrochip, faHdd, faStream, faTrashAlt } from '@fortawesome/free-solid-svg-icons';
|
||||||
import Chart from 'chart.js';
|
import Chart from 'chart.js';
|
||||||
|
import VueJsonPretty from 'vue-json-pretty';
|
||||||
import MkInstanceStats from '../../components/instance-stats.vue';
|
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 { version, url } from '../../config';
|
import { version, url } from '../../config';
|
||||||
import i18n from '../../i18n';
|
import i18n from '../../i18n';
|
||||||
|
|
||||||
@ -92,6 +128,10 @@ export default Vue.extend({
|
|||||||
|
|
||||||
components: {
|
components: {
|
||||||
MkInstanceStats,
|
MkInstanceStats,
|
||||||
|
MkButton,
|
||||||
|
MkSelect,
|
||||||
|
MkInput,
|
||||||
|
VueJsonPretty
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
@ -104,7 +144,10 @@ export default Vue.extend({
|
|||||||
memUsage: 0,
|
memUsage: 0,
|
||||||
chartCpuMem: null,
|
chartCpuMem: null,
|
||||||
chartNet: null,
|
chartNet: null,
|
||||||
faServer, faExchangeAlt, faMicrochip, faHdd
|
logs: [],
|
||||||
|
logLevel: 'all',
|
||||||
|
logDomain: '',
|
||||||
|
faServer, faExchangeAlt, faMicrochip, faHdd, faStream, faTrashAlt
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -114,7 +157,20 @@ export default Vue.extend({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
logLevel() {
|
||||||
|
this.logs = [];
|
||||||
|
this.fetchLogs();
|
||||||
|
},
|
||||||
|
logDomain() {
|
||||||
|
this.logs = [];
|
||||||
|
this.fetchLogs();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
|
this.fetchLogs();
|
||||||
|
|
||||||
Chart.defaults.global.defaultFontColor = getComputedStyle(document.documentElement).getPropertyValue('--fg');
|
Chart.defaults.global.defaultFontColor = getComputedStyle(document.documentElement).getPropertyValue('--fg');
|
||||||
|
|
||||||
this.chartCpuMem = new Chart(this.$refs.cpumem, {
|
this.chartCpuMem = new Chart(this.$refs.cpumem, {
|
||||||
@ -330,6 +386,25 @@ export default Vue.extend({
|
|||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
fetchLogs() {
|
||||||
|
this.$root.api('admin/logs', {
|
||||||
|
level: this.logLevel === 'all' ? null : this.logLevel,
|
||||||
|
domain: this.logDomain === '' ? null : this.logDomain,
|
||||||
|
limit: 30
|
||||||
|
}).then(logs => {
|
||||||
|
this.logs = logs.reverse();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteAllLogs() {
|
||||||
|
this.$root.api('admin/delete-logs').then(() => {
|
||||||
|
this.$root.dialog({
|
||||||
|
type: 'success',
|
||||||
|
iconOnly: true, autoClose: true
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
onStats(stats) {
|
onStats(stats) {
|
||||||
const cpu = (stats.cpu * 100).toFixed(0);
|
const cpu = (stats.cpu * 100).toFixed(0);
|
||||||
const memActive = (stats.mem.active / this.serverInfo.mem.total * 100).toFixed(0);
|
const memActive = (stats.mem.active / this.serverInfo.mem.total * 100).toFixed(0);
|
||||||
@ -389,6 +464,37 @@ export default Vue.extend({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
> .logs {
|
||||||
|
> ._content {
|
||||||
|
> .logs {
|
||||||
|
padding: 8px;
|
||||||
|
background: #000;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 0.9em;
|
||||||
|
|
||||||
|
> code {
|
||||||
|
display: block;
|
||||||
|
|
||||||
|
&.error {
|
||||||
|
color: #f00;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.warning {
|
||||||
|
color: #ff0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.success {
|
||||||
|
color: #0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.debug {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
> .chart {
|
> .chart {
|
||||||
> ._content {
|
> ._content {
|
||||||
> .table {
|
> .table {
|
||||||
|
@ -61,10 +61,10 @@
|
|||||||
<div class="_content">
|
<div class="_content">
|
||||||
<mk-switch v-model="enableServiceWorker">{{ $t('enableServiceworker') }}<template #desc>{{ $t('serviceworkerInfo') }}</template></mk-switch>
|
<mk-switch v-model="enableServiceWorker">{{ $t('enableServiceworker') }}<template #desc>{{ $t('serviceworkerInfo') }}</template></mk-switch>
|
||||||
<template v-if="enableServiceWorker">
|
<template v-if="enableServiceWorker">
|
||||||
<mk-horizon-group inputs class="fit-bottom">
|
<div class="_inputs">
|
||||||
<mk-input v-model="swPublicKey" :disabled="!enableServiceWorker"><template #icon><fa :icon="faKey"/></template>Public key</mk-input>
|
<mk-input v-model="swPublicKey" :disabled="!enableServiceWorker"><template #icon><fa :icon="faKey"/></template>Public key</mk-input>
|
||||||
<mk-input v-model="swPrivateKey" :disabled="!enableServiceWorker"><template #icon><fa :icon="faKey"/></template>Private key</mk-input>
|
<mk-input v-model="swPrivateKey" :disabled="!enableServiceWorker"><template #icon><fa :icon="faKey"/></template>Private key</mk-input>
|
||||||
</mk-horizon-group>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div class="_footer">
|
<div class="_footer">
|
||||||
@ -97,6 +97,33 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section class="_card">
|
||||||
|
<div class="_title"><fa :icon="faCloud"/> {{ $t('objectStorage') }}</div>
|
||||||
|
<div class="_content">
|
||||||
|
<mk-switch v-model="useObjectStorage">{{ $t('useObjectStorage') }}</mk-switch>
|
||||||
|
<template v-if="useObjectStorage">
|
||||||
|
<mk-input v-model="objectStorageBaseUrl" :disabled="!useObjectStorage">URL</mk-input>
|
||||||
|
<div class="_inputs">
|
||||||
|
<mk-input v-model="objectStorageBucket" :disabled="!useObjectStorage">Bucket</mk-input>
|
||||||
|
<mk-input v-model="objectStoragePrefix" :disabled="!useObjectStorage">Prefix</mk-input>
|
||||||
|
</div>
|
||||||
|
<mk-input v-model="objectStorageEndpoint" :disabled="!useObjectStorage">Endpoint</mk-input>
|
||||||
|
<div class="_inputs">
|
||||||
|
<mk-input v-model="objectStorageRegion" :disabled="!useObjectStorage">Region</mk-input>
|
||||||
|
<mk-input v-model="objectStoragePort" type="number" :disabled="!useObjectStorage">Port</mk-input>
|
||||||
|
</div>
|
||||||
|
<div class="_inputs">
|
||||||
|
<mk-input v-model="objectStorageAccessKey" :disabled="!useObjectStorage"><template #icon><fa :icon="faKey"/></template>Access key</mk-input>
|
||||||
|
<mk-input v-model="objectStorageSecretKey" :disabled="!useObjectStorage"><template #icon><fa :icon="faKey"/></template>Secret key</mk-input>
|
||||||
|
</div>
|
||||||
|
<mk-switch v-model="objectStorageUseSSL" :disabled="!useObjectStorage">SSL</mk-switch>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div class="_footer">
|
||||||
|
<mk-button primary @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section class="_card">
|
<section class="_card">
|
||||||
<div class="_title"><fa :icon="faGhost"/> {{ $t('proxyAccount') }}</div>
|
<div class="_title"><fa :icon="faGhost"/> {{ $t('proxyAccount') }}</div>
|
||||||
<div class="_content">
|
<div class="_content">
|
||||||
@ -213,6 +240,16 @@ export default Vue.extend({
|
|||||||
enableServiceWorker: false,
|
enableServiceWorker: false,
|
||||||
swPublicKey: null,
|
swPublicKey: null,
|
||||||
swPrivateKey: null,
|
swPrivateKey: null,
|
||||||
|
useObjectStorage: false,
|
||||||
|
objectStorageBaseUrl: null,
|
||||||
|
objectStorageBucket: null,
|
||||||
|
objectStoragePrefix: null,
|
||||||
|
objectStorageEndpoint: null,
|
||||||
|
objectStorageRegion: null,
|
||||||
|
objectStoragePort: null,
|
||||||
|
objectStorageAccessKey: null,
|
||||||
|
objectStorageSecretKey: null,
|
||||||
|
objectStorageUseSSL: false,
|
||||||
enableTwitterIntegration: false,
|
enableTwitterIntegration: false,
|
||||||
twitterConsumerKey: null,
|
twitterConsumerKey: null,
|
||||||
twitterConsumerSecret: null,
|
twitterConsumerSecret: null,
|
||||||
@ -257,6 +294,16 @@ export default Vue.extend({
|
|||||||
this.enableServiceWorker = this.meta.enableServiceWorker;
|
this.enableServiceWorker = this.meta.enableServiceWorker;
|
||||||
this.swPublicKey = this.meta.swPublickey;
|
this.swPublicKey = this.meta.swPublickey;
|
||||||
this.swPrivateKey = this.meta.swPrivateKey;
|
this.swPrivateKey = this.meta.swPrivateKey;
|
||||||
|
this.useObjectStorage = this.meta.useObjectStorage;
|
||||||
|
this.objectStorageBaseUrl = this.meta.objectStorageBaseUrl;
|
||||||
|
this.objectStorageBucket = this.meta.objectStorageBucket;
|
||||||
|
this.objectStoragePrefix = this.meta.objectStoragePrefix;
|
||||||
|
this.objectStorageEndpoint = this.meta.objectStorageEndpoint;
|
||||||
|
this.objectStorageRegion = this.meta.objectStorageRegion;
|
||||||
|
this.objectStoragePort = this.meta.objectStoragePort;
|
||||||
|
this.objectStorageAccessKey = this.meta.objectStorageAccessKey;
|
||||||
|
this.objectStorageSecretKey = this.meta.objectStorageSecretKey;
|
||||||
|
this.objectStorageUseSSL = this.meta.objectStorageUseSSL;
|
||||||
this.enableTwitterIntegration = this.meta.enableTwitterIntegration;
|
this.enableTwitterIntegration = this.meta.enableTwitterIntegration;
|
||||||
this.twitterConsumerKey = this.meta.twitterConsumerKey;
|
this.twitterConsumerKey = this.meta.twitterConsumerKey;
|
||||||
this.twitterConsumerSecret = this.meta.twitterConsumerSecret;
|
this.twitterConsumerSecret = this.meta.twitterConsumerSecret;
|
||||||
@ -341,6 +388,16 @@ export default Vue.extend({
|
|||||||
enableServiceWorker: this.enableServiceWorker,
|
enableServiceWorker: this.enableServiceWorker,
|
||||||
swPublicKey: this.swPublicKey,
|
swPublicKey: this.swPublicKey,
|
||||||
swPrivateKey: this.swPrivateKey,
|
swPrivateKey: this.swPrivateKey,
|
||||||
|
useObjectStorage: this.useObjectStorage,
|
||||||
|
objectStorageBaseUrl: this.objectStorageBaseUrl ? this.objectStorageBaseUrl : null,
|
||||||
|
objectStorageBucket: this.objectStorageBucket ? this.objectStorageBucket : null,
|
||||||
|
objectStoragePrefix: this.objectStoragePrefix ? this.objectStoragePrefix : null,
|
||||||
|
objectStorageEndpoint: this.objectStorageEndpoint ? this.objectStorageEndpoint : null,
|
||||||
|
objectStorageRegion: this.objectStorageRegion ? this.objectStorageRegion : null,
|
||||||
|
objectStoragePort: this.objectStoragePort ? this.objectStoragePort : null,
|
||||||
|
objectStorageAccessKey: this.objectStorageAccessKey ? this.objectStorageAccessKey : null,
|
||||||
|
objectStorageSecretKey: this.objectStorageSecretKey ? this.objectStorageSecretKey : null,
|
||||||
|
objectStorageUseSSL: this.objectStorageUseSSL,
|
||||||
enableTwitterIntegration: this.enableTwitterIntegration,
|
enableTwitterIntegration: this.enableTwitterIntegration,
|
||||||
twitterConsumerKey: this.twitterConsumerKey,
|
twitterConsumerKey: this.twitterConsumerKey,
|
||||||
twitterConsumerSecret: this.twitterConsumerSecret,
|
twitterConsumerSecret: this.twitterConsumerSecret,
|
||||||
|
@ -13,6 +13,9 @@
|
|||||||
<mk-switch v-model="$store.state.i.autoWatch" @change="onChangeAutoWatch">
|
<mk-switch v-model="$store.state.i.autoWatch" @change="onChangeAutoWatch">
|
||||||
{{ $t('autoNoteWatch') }}<template #desc>{{ $t('autoNoteWatchDescription') }}</template>
|
{{ $t('autoNoteWatch') }}<template #desc>{{ $t('autoNoteWatchDescription') }}</template>
|
||||||
</mk-switch>
|
</mk-switch>
|
||||||
|
<mk-switch v-model="$store.state.i.injectFeaturedNote" @change="onChangeInjectFeaturedNote">
|
||||||
|
{{ $t('showFeaturedNotesInTimeline') }}
|
||||||
|
</mk-switch>
|
||||||
</div>
|
</div>
|
||||||
<div class="_content">
|
<div class="_content">
|
||||||
<mk-button @click="readAllNotifications">{{ $t('markAsReadAllNotifications') }}</mk-button>
|
<mk-button @click="readAllNotifications">{{ $t('markAsReadAllNotifications') }}</mk-button>
|
||||||
@ -84,6 +87,12 @@ export default Vue.extend({
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onChangeInjectFeaturedNote(v) {
|
||||||
|
this.$root.api('i/update', {
|
||||||
|
injectFeaturedNote: v
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
readAllUnreadNotes() {
|
readAllUnreadNotes() {
|
||||||
this.$root.api('i/read_all_unread_notes');
|
this.$root.api('i/read_all_unread_notes');
|
||||||
},
|
},
|
||||||
|
@ -102,7 +102,7 @@ export default Vue.extend({
|
|||||||
}).then(() => {
|
}).then(() => {
|
||||||
this.$root.dialog({
|
this.$root.dialog({
|
||||||
type: 'success',
|
type: 'success',
|
||||||
splash: true
|
iconOnly: true, autoClose: true
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -67,7 +67,7 @@ export default (opts) => ({
|
|||||||
...params,
|
...params,
|
||||||
limit: this.pagination.noPaging ? (this.pagination.limit || 10) : (this.pagination.limit || 10) + 1,
|
limit: this.pagination.noPaging ? (this.pagination.limit || 10) : (this.pagination.limit || 10) + 1,
|
||||||
}).then(items => {
|
}).then(items => {
|
||||||
if (!this.pagination.noPaging && (items.length === (this.pagination.limit || 10) + 1)) {
|
if (!this.pagination.noPaging && (items.length > (this.pagination.limit || 10))) {
|
||||||
items.pop();
|
items.pop();
|
||||||
this.items = this.pagination.reversed ? [...items].reverse() : items;
|
this.items = this.pagination.reversed ? [...items].reverse() : items;
|
||||||
this.more = true;
|
this.more = true;
|
||||||
@ -103,7 +103,7 @@ export default (opts) => ({
|
|||||||
untilId: this.items[this.items.length - 1].id,
|
untilId: this.items[this.items.length - 1].id,
|
||||||
}),
|
}),
|
||||||
}).then(items => {
|
}).then(items => {
|
||||||
if (items.length === SECOND_FETCH_LIMIT + 1) {
|
if (items.length > SECOND_FETCH_LIMIT) {
|
||||||
items.pop();
|
items.pop();
|
||||||
this.items = this.pagination.reversed ? [...items].reverse().concat(this.items) : this.items.concat(items);
|
this.items = this.pagination.reversed ? [...items].reverse().concat(this.items) : this.items.concat(items);
|
||||||
this.more = true;
|
this.more = true;
|
||||||
|
@ -248,6 +248,32 @@ hr {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
._inputs {
|
||||||
|
display: flex;
|
||||||
|
margin: 32px 0;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
> * {
|
||||||
|
flex: 1;
|
||||||
|
margin: 0 !important;
|
||||||
|
|
||||||
|
&:not(:first-child) {
|
||||||
|
margin-left: 8px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(:last-child) {
|
||||||
|
margin-right: 8px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
._shadow {
|
._shadow {
|
||||||
box-shadow: 0 8px 32px var(--shadow);
|
box-shadow: 0 8px 32px var(--shadow);
|
||||||
|
|
||||||
|
84
src/client/widgets/activity.calendar.vue
Normal file
84
src/client/widgets/activity.calendar.vue
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
<template>
|
||||||
|
<svg viewBox="0 0 21 7">
|
||||||
|
<rect v-for="record in data" class="day"
|
||||||
|
width="1" height="1"
|
||||||
|
:x="record.x" :y="record.date.weekday"
|
||||||
|
rx="1" ry="1"
|
||||||
|
fill="transparent">
|
||||||
|
<title>{{ record.date.year }}/{{ record.date.month + 1 }}/{{ record.date.day }}</title>
|
||||||
|
</rect>
|
||||||
|
<rect v-for="record in data" class="day"
|
||||||
|
:width="record.v" :height="record.v"
|
||||||
|
:x="record.x + ((1 - record.v) / 2)" :y="record.date.weekday + ((1 - record.v) / 2)"
|
||||||
|
rx="1" ry="1"
|
||||||
|
:fill="record.color"
|
||||||
|
style="pointer-events: none;"/>
|
||||||
|
<rect class="today"
|
||||||
|
width="1" height="1"
|
||||||
|
:x="data[0].x" :y="data[0].date.weekday"
|
||||||
|
rx="1" ry="1"
|
||||||
|
fill="none"
|
||||||
|
stroke-width="0.1"
|
||||||
|
stroke="#f73520"/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import Vue from 'vue';
|
||||||
|
|
||||||
|
export default Vue.extend({
|
||||||
|
props: ['data'],
|
||||||
|
created() {
|
||||||
|
for (const d of this.data) {
|
||||||
|
d.total = d.notes + d.replies + d.renotes;
|
||||||
|
}
|
||||||
|
const peak = Math.max.apply(null, this.data.map(d => d.total));
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
const year = now.getFullYear();
|
||||||
|
const month = now.getMonth();
|
||||||
|
const day = now.getDate();
|
||||||
|
|
||||||
|
let x = 20;
|
||||||
|
this.data.slice().forEach((d, i) => {
|
||||||
|
d.x = x;
|
||||||
|
|
||||||
|
const date = new Date(year, month, day - i);
|
||||||
|
d.date = {
|
||||||
|
year: date.getFullYear(),
|
||||||
|
month: date.getMonth(),
|
||||||
|
day: date.getDate(),
|
||||||
|
weekday: date.getDay()
|
||||||
|
};
|
||||||
|
|
||||||
|
d.v = peak == 0 ? 0 : d.total / (peak / 2);
|
||||||
|
if (d.v > 1) d.v = 1;
|
||||||
|
const ch = d.date.weekday == 0 || d.date.weekday == 6 ? 275 : 170;
|
||||||
|
const cs = d.v * 100;
|
||||||
|
const cl = 15 + ((1 - d.v) * 80);
|
||||||
|
d.color = `hsl(${ch}, ${cs}%, ${cl}%)`;
|
||||||
|
|
||||||
|
if (d.date.weekday == 0) x--;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
svg {
|
||||||
|
display: block;
|
||||||
|
padding: 16px;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
> rect {
|
||||||
|
transform-origin: center;
|
||||||
|
|
||||||
|
&.day {
|
||||||
|
&:hover {
|
||||||
|
fill: rgba(#000, 0.05);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
108
src/client/widgets/activity.chart.vue
Normal file
108
src/client/widgets/activity.chart.vue
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
<template>
|
||||||
|
<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`" @mousedown.prevent="onMousedown">
|
||||||
|
<polyline
|
||||||
|
:points="pointsNote"
|
||||||
|
fill="none"
|
||||||
|
stroke-width="1"
|
||||||
|
stroke="#41ddde"/>
|
||||||
|
<polyline
|
||||||
|
:points="pointsReply"
|
||||||
|
fill="none"
|
||||||
|
stroke-width="1"
|
||||||
|
stroke="#f7796c"/>
|
||||||
|
<polyline
|
||||||
|
:points="pointsRenote"
|
||||||
|
fill="none"
|
||||||
|
stroke-width="1"
|
||||||
|
stroke="#a1de41"/>
|
||||||
|
<polyline
|
||||||
|
:points="pointsTotal"
|
||||||
|
fill="none"
|
||||||
|
stroke-width="1"
|
||||||
|
stroke="#555"
|
||||||
|
stroke-dasharray="2 2"/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import Vue from 'vue';
|
||||||
|
import i18n from '../i18n';
|
||||||
|
|
||||||
|
function dragListen(fn) {
|
||||||
|
window.addEventListener('mousemove', fn);
|
||||||
|
window.addEventListener('mouseleave', dragClear.bind(null, fn));
|
||||||
|
window.addEventListener('mouseup', dragClear.bind(null, fn));
|
||||||
|
}
|
||||||
|
|
||||||
|
function dragClear(fn) {
|
||||||
|
window.removeEventListener('mousemove', fn);
|
||||||
|
window.removeEventListener('mouseleave', dragClear);
|
||||||
|
window.removeEventListener('mouseup', dragClear);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Vue.extend({
|
||||||
|
i18n,
|
||||||
|
props: ['data'],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
viewBoxX: 147,
|
||||||
|
viewBoxY: 60,
|
||||||
|
zoom: 1,
|
||||||
|
pos: 0,
|
||||||
|
pointsNote: null,
|
||||||
|
pointsReply: null,
|
||||||
|
pointsRenote: null,
|
||||||
|
pointsTotal: null
|
||||||
|
};
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
for (const d of this.data) {
|
||||||
|
d.total = d.notes + d.replies + d.renotes;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.render();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
render() {
|
||||||
|
const peak = Math.max.apply(null, this.data.map(d => d.total));
|
||||||
|
if (peak != 0) {
|
||||||
|
const data = this.data.slice().reverse();
|
||||||
|
this.pointsNote = data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.notes / peak)) * this.viewBoxY}`).join(' ');
|
||||||
|
this.pointsReply = data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.replies / peak)) * this.viewBoxY}`).join(' ');
|
||||||
|
this.pointsRenote = data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.renotes / peak)) * this.viewBoxY}`).join(' ');
|
||||||
|
this.pointsTotal = data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.total / peak)) * this.viewBoxY}`).join(' ');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onMousedown(e) {
|
||||||
|
const clickX = e.clientX;
|
||||||
|
const clickY = e.clientY;
|
||||||
|
const baseZoom = this.zoom;
|
||||||
|
const basePos = this.pos;
|
||||||
|
|
||||||
|
// 動かした時
|
||||||
|
dragListen(me => {
|
||||||
|
let moveLeft = me.clientX - clickX;
|
||||||
|
let moveTop = me.clientY - clickY;
|
||||||
|
|
||||||
|
this.zoom = baseZoom + (-moveTop / 20);
|
||||||
|
this.pos = basePos + moveLeft;
|
||||||
|
if (this.zoom < 1) this.zoom = 1;
|
||||||
|
if (this.pos > 0) this.pos = 0;
|
||||||
|
if (this.pos < -(((this.data.length - 1) * this.zoom) - this.viewBoxX)) this.pos = -(((this.data.length - 1) * this.zoom) - this.viewBoxX);
|
||||||
|
|
||||||
|
this.render();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
svg {
|
||||||
|
display: block;
|
||||||
|
padding: 16px;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
cursor: all-scroll;
|
||||||
|
}
|
||||||
|
</style>
|
80
src/client/widgets/activity.vue
Normal file
80
src/client/widgets/activity.vue
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<mk-container :show-header="props.design === 0" :naked="props.design === 2">
|
||||||
|
<template #header><fa :icon="faChartBar"/>{{ $t('_widgets.activity') }}</template>
|
||||||
|
<template #func><button @click="toggleView()" class="_button"><fa :icon="faSort"/></button></template>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<mk-loading v-if="fetching"/>
|
||||||
|
<template v-else>
|
||||||
|
<x-calendar v-show="props.view === 0" :data="[].concat(activity)"/>
|
||||||
|
<x-chart v-show="props.view === 1" :data="[].concat(activity)"/>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</mk-container>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { faChartBar, faSort } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import MkContainer from '../components/ui/container.vue';
|
||||||
|
import define from './define';
|
||||||
|
import i18n from '../i18n';
|
||||||
|
import XCalendar from './activity.calendar.vue';
|
||||||
|
import XChart from './activity.chart.vue';
|
||||||
|
|
||||||
|
export default define({
|
||||||
|
name: 'activity',
|
||||||
|
props: () => ({
|
||||||
|
design: 0,
|
||||||
|
view: 0
|
||||||
|
})
|
||||||
|
}).extend({
|
||||||
|
i18n,
|
||||||
|
components: {
|
||||||
|
MkContainer,
|
||||||
|
XCalendar,
|
||||||
|
XChart,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
fetching: true,
|
||||||
|
activity: null,
|
||||||
|
faChartBar, faSort
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.$root.api('charts/user/notes', {
|
||||||
|
userId: this.$store.state.i.id,
|
||||||
|
span: 'day',
|
||||||
|
limit: 7 * 21
|
||||||
|
}).then(activity => {
|
||||||
|
this.activity = activity.diffs.normal.map((_, i) => ({
|
||||||
|
total: activity.diffs.normal[i] + activity.diffs.reply[i] + activity.diffs.renote[i],
|
||||||
|
notes: activity.diffs.normal[i],
|
||||||
|
replies: activity.diffs.reply[i],
|
||||||
|
renotes: activity.diffs.renote[i]
|
||||||
|
}));
|
||||||
|
this.fetching = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
func() {
|
||||||
|
if (this.props.design == 2) {
|
||||||
|
this.props.design = 0;
|
||||||
|
} else {
|
||||||
|
this.props.design++;
|
||||||
|
}
|
||||||
|
this.save();
|
||||||
|
},
|
||||||
|
toggleView() {
|
||||||
|
if (this.props.view == 1) {
|
||||||
|
this.props.view = 0;
|
||||||
|
} else {
|
||||||
|
this.props.view++;
|
||||||
|
}
|
||||||
|
this.save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
@ -7,3 +7,5 @@ Vue.component('mkw-calendar', () => import('./calendar.vue').then(m => m.default
|
|||||||
Vue.component('mkw-rss', () => import('./rss.vue').then(m => m.default));
|
Vue.component('mkw-rss', () => import('./rss.vue').then(m => m.default));
|
||||||
Vue.component('mkw-trends', () => import('./trends.vue').then(m => m.default));
|
Vue.component('mkw-trends', () => import('./trends.vue').then(m => m.default));
|
||||||
Vue.component('mkw-clock', () => import('./clock.vue').then(m => m.default));
|
Vue.component('mkw-clock', () => import('./clock.vue').then(m => m.default));
|
||||||
|
Vue.component('mkw-activity', () => import('./activity.vue').then(m => m.default));
|
||||||
|
Vue.component('mkw-photos', () => import('./photos.vue').then(m => m.default));
|
||||||
|
116
src/client/widgets/photos.vue
Normal file
116
src/client/widgets/photos.vue
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<mk-container :show-header="props.design === 0" :naked="props.design === 2" :class="$style.root" :data-melt="props.design == 2">
|
||||||
|
<template #header><fa :icon="faCamera"/>{{ $t('_widgets.photos') }}</template>
|
||||||
|
|
||||||
|
<div class="">
|
||||||
|
<mk-loading v-if="fetching"/>
|
||||||
|
<div v-else :class="$style.stream">
|
||||||
|
<div v-for="(image, i) in images" :key="i"
|
||||||
|
:class="$style.img"
|
||||||
|
:style="`background-image: url(${thumbnail(image)})`"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</mk-container>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { faCamera } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import MkContainer from '../components/ui/container.vue';
|
||||||
|
import define from './define';
|
||||||
|
import i18n from '../i18n';
|
||||||
|
import { getStaticImageUrl } from '../scripts/get-static-image-url';
|
||||||
|
|
||||||
|
export default define({
|
||||||
|
name: 'photos',
|
||||||
|
props: () => ({
|
||||||
|
design: 0,
|
||||||
|
})
|
||||||
|
}).extend({
|
||||||
|
i18n,
|
||||||
|
components: {
|
||||||
|
MkContainer,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
images: [],
|
||||||
|
fetching: true,
|
||||||
|
connection: null,
|
||||||
|
faCamera
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.connection = this.$root.stream.useSharedConnection('main');
|
||||||
|
|
||||||
|
this.connection.on('driveFileCreated', this.onDriveFileCreated);
|
||||||
|
|
||||||
|
this.$root.api('drive/stream', {
|
||||||
|
type: 'image/*',
|
||||||
|
limit: 9
|
||||||
|
}).then(images => {
|
||||||
|
this.images = images;
|
||||||
|
this.fetching = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
this.connection.dispose();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onDriveFileCreated(file) {
|
||||||
|
if (/^image\/.+$/.test(file.type)) {
|
||||||
|
this.images.unshift(file);
|
||||||
|
if (this.images.length > 9) this.images.pop();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
func() {
|
||||||
|
if (this.props.design == 2) {
|
||||||
|
this.props.design = 0;
|
||||||
|
} else {
|
||||||
|
this.props.design++;
|
||||||
|
}
|
||||||
|
this.save();
|
||||||
|
},
|
||||||
|
|
||||||
|
thumbnail(image: any): string {
|
||||||
|
return this.$store.state.device.disableShowingAnimatedImages
|
||||||
|
? getStaticImageUrl(image.thumbnailUrl)
|
||||||
|
: image.thumbnailUrl;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.root[data-melt] {
|
||||||
|
.stream {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.img {
|
||||||
|
border: solid 4px transparent;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.stream {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
padding: 8px;
|
||||||
|
|
||||||
|
.img {
|
||||||
|
flex: 1 1 33%;
|
||||||
|
width: 33%;
|
||||||
|
height: 80px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background-position: center center;
|
||||||
|
background-size: cover;
|
||||||
|
background-clip: content-box;
|
||||||
|
border: solid 2px transparent;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -55,6 +55,8 @@ import { Clip } from '../models/entities/clip';
|
|||||||
import { ClipNote } from '../models/entities/clip-note';
|
import { ClipNote } from '../models/entities/clip-note';
|
||||||
import { Antenna } from '../models/entities/antenna';
|
import { Antenna } from '../models/entities/antenna';
|
||||||
import { AntennaNote } from '../models/entities/antenna-note';
|
import { AntennaNote } from '../models/entities/antenna-note';
|
||||||
|
import { PromoNote } from '../models/entities/promo-note';
|
||||||
|
import { PromoRead } from '../models/entities/promo-read';
|
||||||
|
|
||||||
const sqlLogger = dbLogger.createSubLogger('sql', 'white', false);
|
const sqlLogger = dbLogger.createSubLogger('sql', 'white', false);
|
||||||
|
|
||||||
@ -140,6 +142,8 @@ export const entities = [
|
|||||||
ClipNote,
|
ClipNote,
|
||||||
Antenna,
|
Antenna,
|
||||||
AntennaNote,
|
AntennaNote,
|
||||||
|
PromoNote,
|
||||||
|
PromoRead,
|
||||||
ReversiGame,
|
ReversiGame,
|
||||||
ReversiMatching,
|
ReversiMatching,
|
||||||
...charts as any
|
...charts as any
|
||||||
|
28
src/models/entities/promo-note.ts
Normal file
28
src/models/entities/promo-note.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { PrimaryColumn, Entity, Index, JoinColumn, Column, OneToOne } from 'typeorm';
|
||||||
|
import { Note } from './note';
|
||||||
|
import { User } from './user';
|
||||||
|
import { id } from '../id';
|
||||||
|
|
||||||
|
@Entity()
|
||||||
|
export class PromoNote {
|
||||||
|
@PrimaryColumn(id())
|
||||||
|
public noteId: Note['id'];
|
||||||
|
|
||||||
|
@OneToOne(type => Note, {
|
||||||
|
onDelete: 'CASCADE'
|
||||||
|
})
|
||||||
|
@JoinColumn()
|
||||||
|
public note: Note | null;
|
||||||
|
|
||||||
|
@Column('timestamp with time zone')
|
||||||
|
public expiresAt: Date;
|
||||||
|
|
||||||
|
//#region Denormalized fields
|
||||||
|
@Index()
|
||||||
|
@Column({
|
||||||
|
...id(),
|
||||||
|
comment: '[Denormalized]'
|
||||||
|
})
|
||||||
|
public userId: User['id'];
|
||||||
|
//#endregion
|
||||||
|
}
|
35
src/models/entities/promo-read.ts
Normal file
35
src/models/entities/promo-read.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
|
||||||
|
import { Note } from './note';
|
||||||
|
import { User } from './user';
|
||||||
|
import { id } from '../id';
|
||||||
|
|
||||||
|
@Entity()
|
||||||
|
@Index(['userId', 'noteId'], { unique: true })
|
||||||
|
export class PromoRead {
|
||||||
|
@PrimaryColumn(id())
|
||||||
|
public id: string;
|
||||||
|
|
||||||
|
@Column('timestamp with time zone', {
|
||||||
|
comment: 'The created date of the PromoRead.'
|
||||||
|
})
|
||||||
|
public createdAt: Date;
|
||||||
|
|
||||||
|
@Index()
|
||||||
|
@Column(id())
|
||||||
|
public userId: User['id'];
|
||||||
|
|
||||||
|
@ManyToOne(type => User, {
|
||||||
|
onDelete: 'CASCADE'
|
||||||
|
})
|
||||||
|
@JoinColumn()
|
||||||
|
public user: User | null;
|
||||||
|
|
||||||
|
@Column(id())
|
||||||
|
public noteId: Note['id'];
|
||||||
|
|
||||||
|
@ManyToOne(type => Note, {
|
||||||
|
onDelete: 'CASCADE'
|
||||||
|
})
|
||||||
|
@JoinColumn()
|
||||||
|
public note: Note | null;
|
||||||
|
}
|
@ -125,6 +125,11 @@ export class UserProfile {
|
|||||||
})
|
})
|
||||||
public carefulBot: boolean;
|
public carefulBot: boolean;
|
||||||
|
|
||||||
|
@Column('boolean', {
|
||||||
|
default: true,
|
||||||
|
})
|
||||||
|
public injectFeaturedNote: boolean;
|
||||||
|
|
||||||
@Column({
|
@Column({
|
||||||
...id(),
|
...id(),
|
||||||
nullable: true
|
nullable: true
|
||||||
|
@ -50,6 +50,8 @@ import { ClipRepository } from './repositories/clip';
|
|||||||
import { ClipNote } from './entities/clip-note';
|
import { ClipNote } from './entities/clip-note';
|
||||||
import { AntennaRepository } from './repositories/antenna';
|
import { AntennaRepository } from './repositories/antenna';
|
||||||
import { AntennaNote } from './entities/antenna-note';
|
import { AntennaNote } from './entities/antenna-note';
|
||||||
|
import { PromoNote } from './entities/promo-note';
|
||||||
|
import { PromoRead } from './entities/promo-read';
|
||||||
|
|
||||||
export const Announcements = getRepository(Announcement);
|
export const Announcements = getRepository(Announcement);
|
||||||
export const AnnouncementReads = getRepository(AnnouncementRead);
|
export const AnnouncementReads = getRepository(AnnouncementRead);
|
||||||
@ -102,3 +104,5 @@ export const Clips = getCustomRepository(ClipRepository);
|
|||||||
export const ClipNotes = getRepository(ClipNote);
|
export const ClipNotes = getRepository(ClipNote);
|
||||||
export const Antennas = getCustomRepository(AntennaRepository);
|
export const Antennas = getCustomRepository(AntennaRepository);
|
||||||
export const AntennaNotes = getRepository(AntennaNote);
|
export const AntennaNotes = getRepository(AntennaNote);
|
||||||
|
export const PromoNotes = getRepository(PromoNote);
|
||||||
|
export const PromoReads = getRepository(PromoRead);
|
||||||
|
@ -196,6 +196,8 @@ export class NoteRepository extends Repository<Note> {
|
|||||||
renoteId: note.renoteId,
|
renoteId: note.renoteId,
|
||||||
mentions: note.mentions.length > 0 ? note.mentions : undefined,
|
mentions: note.mentions.length > 0 ? note.mentions : undefined,
|
||||||
uri: note.uri || undefined,
|
uri: note.uri || undefined,
|
||||||
|
_featuredId_: (note as any)._featuredId_ || undefined,
|
||||||
|
_prId_: (note as any)._prId_ || undefined,
|
||||||
|
|
||||||
...(opts.detail ? {
|
...(opts.detail ? {
|
||||||
reply: note.replyId ? this.pack(note.replyId, meId, {
|
reply: note.replyId ? this.pack(note.replyId, meId, {
|
||||||
|
@ -227,6 +227,7 @@ export class UserRepository extends Repository<User> {
|
|||||||
avatarId: user.avatarId,
|
avatarId: user.avatarId,
|
||||||
bannerId: user.bannerId,
|
bannerId: user.bannerId,
|
||||||
autoWatch: profile!.autoWatch,
|
autoWatch: profile!.autoWatch,
|
||||||
|
injectFeaturedNote: profile!.injectFeaturedNote,
|
||||||
alwaysMarkNsfw: profile!.alwaysMarkNsfw,
|
alwaysMarkNsfw: profile!.alwaysMarkNsfw,
|
||||||
carefulBot: profile!.carefulBot,
|
carefulBot: profile!.carefulBot,
|
||||||
autoAcceptFollowed: profile!.autoAcceptFollowed,
|
autoAcceptFollowed: profile!.autoAcceptFollowed,
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { User } from '../../../models/entities/user';
|
import { User } from '../../../models/entities/user';
|
||||||
import { Brackets, SelectQueryBuilder } from 'typeorm';
|
import { Brackets, SelectQueryBuilder } from 'typeorm';
|
||||||
|
|
||||||
export function generateRepliesQuery(q: SelectQueryBuilder<any>, me?: User) {
|
export function generateRepliesQuery(q: SelectQueryBuilder<any>, me?: User | null) {
|
||||||
if (me == null) {
|
if (me == null) {
|
||||||
q.andWhere(new Brackets(qb => { qb
|
q.andWhere(new Brackets(qb => { qb
|
||||||
.where(`note.replyId IS NULL`) // 返信ではない
|
.where(`note.replyId IS NULL`) // 返信ではない
|
||||||
|
@ -2,7 +2,7 @@ import { User } from '../../../models/entities/user';
|
|||||||
import { Followings } from '../../../models';
|
import { Followings } from '../../../models';
|
||||||
import { Brackets, SelectQueryBuilder } from 'typeorm';
|
import { Brackets, SelectQueryBuilder } from 'typeorm';
|
||||||
|
|
||||||
export function generateVisibilityQuery(q: SelectQueryBuilder<any>, me?: User) {
|
export function generateVisibilityQuery(q: SelectQueryBuilder<any>, me?: User | null) {
|
||||||
if (me == null) {
|
if (me == null) {
|
||||||
q.andWhere(new Brackets(qb => { qb
|
q.andWhere(new Brackets(qb => { qb
|
||||||
.where(`note.visibility = 'public'`)
|
.where(`note.visibility = 'public'`)
|
||||||
|
45
src/server/api/common/inject-featured.ts
Normal file
45
src/server/api/common/inject-featured.ts
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import rndstr from 'rndstr';
|
||||||
|
import { Note } from '../../../models/entities/note';
|
||||||
|
import { User } from '../../../models/entities/user';
|
||||||
|
import { Notes, UserProfiles } from '../../../models';
|
||||||
|
import { generateMuteQuery } from './generate-mute-query';
|
||||||
|
import { ensure } from '../../../prelude/ensure';
|
||||||
|
|
||||||
|
// TODO: リアクション、Renote、返信などをしたノートは除外する
|
||||||
|
|
||||||
|
export async function injectFeatured(timeline: Note[], user?: User | null) {
|
||||||
|
if (timeline.length < 5) return;
|
||||||
|
|
||||||
|
if (user) {
|
||||||
|
const profile = await UserProfiles.findOne(user.id).then(ensure);
|
||||||
|
if (!profile.injectFeaturedNote) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const max = 30;
|
||||||
|
const day = 1000 * 60 * 60 * 24 * 3; // 3日前まで
|
||||||
|
|
||||||
|
const query = Notes.createQueryBuilder('note')
|
||||||
|
.addSelect('note.score')
|
||||||
|
.where('note.userHost IS NULL')
|
||||||
|
.andWhere(`note.score > 0`)
|
||||||
|
.andWhere(`note.createdAt > :date`, { date: new Date(Date.now() - day) })
|
||||||
|
.andWhere(`note.visibility = 'public'`)
|
||||||
|
.leftJoinAndSelect('note.user', 'user');
|
||||||
|
|
||||||
|
if (user) generateMuteQuery(query, user);
|
||||||
|
|
||||||
|
const notes = await query
|
||||||
|
.orderBy('note.score', 'DESC')
|
||||||
|
.take(max)
|
||||||
|
.getMany();
|
||||||
|
|
||||||
|
if (notes.length === 0) return;
|
||||||
|
|
||||||
|
// Pick random one
|
||||||
|
const featured = notes[Math.floor(Math.random() * notes.length)];
|
||||||
|
|
||||||
|
(featured as any)._featuredId_ = rndstr('a-z0-9', 8);
|
||||||
|
|
||||||
|
// Inject featured
|
||||||
|
timeline.splice(3, 0, featured);
|
||||||
|
}
|
35
src/server/api/common/inject-promo.ts
Normal file
35
src/server/api/common/inject-promo.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import rndstr from 'rndstr';
|
||||||
|
import { Note } from '../../../models/entities/note';
|
||||||
|
import { User } from '../../../models/entities/user';
|
||||||
|
import { PromoReads, PromoNotes, Notes, Users } from '../../../models';
|
||||||
|
import { ensure } from '../../../prelude/ensure';
|
||||||
|
|
||||||
|
export async function injectPromo(timeline: Note[], user?: User | null) {
|
||||||
|
if (timeline.length < 5) return;
|
||||||
|
|
||||||
|
// TODO: readやexpireフィルタはクエリ側でやる
|
||||||
|
|
||||||
|
const reads = user ? await PromoReads.find({
|
||||||
|
userId: user.id
|
||||||
|
}) : [];
|
||||||
|
|
||||||
|
let promos = await PromoNotes.find();
|
||||||
|
|
||||||
|
promos = promos.filter(n => n.expiresAt.getTime() > Date.now());
|
||||||
|
promos = promos.filter(n => !reads.map(r => r.noteId).includes(n.noteId));
|
||||||
|
|
||||||
|
if (promos.length === 0) return;
|
||||||
|
|
||||||
|
// Pick random promo
|
||||||
|
const promo = promos[Math.floor(Math.random() * promos.length)];
|
||||||
|
|
||||||
|
const note = await Notes.findOne(promo.noteId).then(ensure);
|
||||||
|
|
||||||
|
// Join
|
||||||
|
note.user = await Users.findOne(note.userId).then(ensure);
|
||||||
|
|
||||||
|
(note as any)._prId_ = rndstr('a-z0-9', 8);
|
||||||
|
|
||||||
|
// Inject promo
|
||||||
|
timeline.splice(3, 0, note);
|
||||||
|
}
|
@ -88,7 +88,6 @@ export async function signup(username: User['username'], password: UserProfile['
|
|||||||
await transactionalEntityManager.save(new UserProfile({
|
await transactionalEntityManager.save(new UserProfile({
|
||||||
userId: account.id,
|
userId: account.id,
|
||||||
autoAcceptFollowed: true,
|
autoAcceptFollowed: true,
|
||||||
autoWatch: false,
|
|
||||||
password: hash,
|
password: hash,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@ import { ApiError } from './error';
|
|||||||
import { App } from '../../models/entities/app';
|
import { App } from '../../models/entities/app';
|
||||||
import { SchemaType } from '../../misc/schema';
|
import { SchemaType } from '../../misc/schema';
|
||||||
|
|
||||||
|
// TODO: defaultが設定されている場合はその型も考慮する
|
||||||
type Params<T extends IEndpointMeta> = {
|
type Params<T extends IEndpointMeta> = {
|
||||||
[P in keyof T['params']]: NonNullable<T['params']>[P]['transform'] extends Function
|
[P in keyof T['params']]: NonNullable<T['params']>[P]['transform'] extends Function
|
||||||
? ReturnType<NonNullable<T['params']>[P]['transform']>
|
? ReturnType<NonNullable<T['params']>[P]['transform']>
|
||||||
|
@ -32,6 +32,10 @@ export default define(meta, async (ps) => {
|
|||||||
throw new Error('user not found');
|
throw new Error('user not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (user.isAdmin) {
|
||||||
|
throw new Error('cannot mark as moderator if admin user');
|
||||||
|
}
|
||||||
|
|
||||||
await Users.update(user.id, {
|
await Users.update(user.id, {
|
||||||
isModerator: true
|
isModerator: true
|
||||||
});
|
});
|
||||||
|
58
src/server/api/endpoints/admin/promo/create.ts
Normal file
58
src/server/api/endpoints/admin/promo/create.ts
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import $ from 'cafy';
|
||||||
|
import { ID } from '../../../../../misc/cafy-id';
|
||||||
|
import define from '../../../define';
|
||||||
|
import { ApiError } from '../../../error';
|
||||||
|
import { getNote } from '../../../common/getters';
|
||||||
|
import { PromoNotes } from '../../../../../models';
|
||||||
|
|
||||||
|
export const meta = {
|
||||||
|
requireCredential: true as const,
|
||||||
|
requireModerator: true,
|
||||||
|
|
||||||
|
params: {
|
||||||
|
noteId: {
|
||||||
|
validator: $.type(ID),
|
||||||
|
},
|
||||||
|
|
||||||
|
expiresAt: {
|
||||||
|
validator: $.num.int()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
errors: {
|
||||||
|
noSuchNote: {
|
||||||
|
message: 'No such note.',
|
||||||
|
code: 'NO_SUCH_NOTE',
|
||||||
|
id: 'ee449fbe-af2a-453b-9cae-cf2fe7c895fc'
|
||||||
|
},
|
||||||
|
|
||||||
|
alreadyPromoted: {
|
||||||
|
message: 'The note has already promoted.',
|
||||||
|
code: 'ALREADY_PROMOTED',
|
||||||
|
id: 'ae427aa2-7a41-484f-a18c-2c1104051604'
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default define(meta, async (ps, user) => {
|
||||||
|
// Get favoritee
|
||||||
|
const note = await getNote(ps.noteId).catch(e => {
|
||||||
|
if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
|
||||||
|
throw e;
|
||||||
|
});
|
||||||
|
|
||||||
|
// if already favorited
|
||||||
|
const exist = await PromoNotes.findOne(note.id);
|
||||||
|
|
||||||
|
if (exist != null) {
|
||||||
|
throw new ApiError(meta.errors.alreadyPromoted);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create favorite
|
||||||
|
await PromoNotes.save({
|
||||||
|
noteId: note.id,
|
||||||
|
createdAt: new Date(),
|
||||||
|
expiresAt: new Date(ps.expiresAt),
|
||||||
|
userId: note.userId,
|
||||||
|
});
|
||||||
|
});
|
@ -31,7 +31,7 @@ export default define(meta, async (ps, me) => {
|
|||||||
throw new Error('user not found');
|
throw new Error('user not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (me.isModerator && user.isAdmin) {
|
if ((me.isModerator && !me.isAdmin) && user.isAdmin) {
|
||||||
throw new Error('cannot show info of admin');
|
throw new Error('cannot show info of admin');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,8 +8,6 @@ import { getUser } from '../../common/getters';
|
|||||||
import { Blockings, NoteWatchings, Users } from '../../../../models';
|
import { Blockings, NoteWatchings, Users } from '../../../../models';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
stability: 'stable',
|
|
||||||
|
|
||||||
desc: {
|
desc: {
|
||||||
'ja-JP': '指定したユーザーをブロックします。',
|
'ja-JP': '指定したユーザーをブロックします。',
|
||||||
'en-US': 'Block a user.'
|
'en-US': 'Block a user.'
|
||||||
|
@ -8,8 +8,6 @@ import { getUser } from '../../common/getters';
|
|||||||
import { Blockings, Users } from '../../../../models';
|
import { Blockings, Users } from '../../../../models';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
stability: 'stable',
|
|
||||||
|
|
||||||
desc: {
|
desc: {
|
||||||
'ja-JP': '指定したユーザーのブロックを解除します。',
|
'ja-JP': '指定したユーザーのブロックを解除します。',
|
||||||
'en-US': 'Unblock a user.'
|
'en-US': 'Unblock a user.'
|
||||||
|
@ -4,8 +4,6 @@ import { convertLog } from '../../../../services/chart/core';
|
|||||||
import { activeUsersChart } from '../../../../services/chart';
|
import { activeUsersChart } from '../../../../services/chart';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
stability: 'stable',
|
|
||||||
|
|
||||||
desc: {
|
desc: {
|
||||||
'ja-JP': 'アクティブユーザーのチャートを取得します。'
|
'ja-JP': 'アクティブユーザーのチャートを取得します。'
|
||||||
},
|
},
|
||||||
|
@ -4,8 +4,6 @@ import { convertLog } from '../../../../services/chart/core';
|
|||||||
import { driveChart } from '../../../../services/chart';
|
import { driveChart } from '../../../../services/chart';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
stability: 'stable',
|
|
||||||
|
|
||||||
desc: {
|
desc: {
|
||||||
'ja-JP': 'ドライブのチャートを取得します。'
|
'ja-JP': 'ドライブのチャートを取得します。'
|
||||||
},
|
},
|
||||||
|
@ -4,8 +4,6 @@ import { convertLog } from '../../../../services/chart/core';
|
|||||||
import { federationChart } from '../../../../services/chart';
|
import { federationChart } from '../../../../services/chart';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
stability: 'stable',
|
|
||||||
|
|
||||||
desc: {
|
desc: {
|
||||||
'ja-JP': 'フェデレーションのチャートを取得します。'
|
'ja-JP': 'フェデレーションのチャートを取得します。'
|
||||||
},
|
},
|
||||||
|
@ -4,8 +4,6 @@ import { convertLog } from '../../../../services/chart/core';
|
|||||||
import { hashtagChart } from '../../../../services/chart';
|
import { hashtagChart } from '../../../../services/chart';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
stability: 'stable',
|
|
||||||
|
|
||||||
desc: {
|
desc: {
|
||||||
'ja-JP': 'ハッシュタグごとのチャートを取得します。'
|
'ja-JP': 'ハッシュタグごとのチャートを取得します。'
|
||||||
},
|
},
|
||||||
|
@ -4,8 +4,6 @@ import { convertLog } from '../../../../services/chart/core';
|
|||||||
import { instanceChart } from '../../../../services/chart';
|
import { instanceChart } from '../../../../services/chart';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
stability: 'stable',
|
|
||||||
|
|
||||||
desc: {
|
desc: {
|
||||||
'ja-JP': 'インスタンスごとのチャートを取得します。'
|
'ja-JP': 'インスタンスごとのチャートを取得します。'
|
||||||
},
|
},
|
||||||
|
@ -4,8 +4,6 @@ import { convertLog } from '../../../../services/chart/core';
|
|||||||
import { networkChart } from '../../../../services/chart';
|
import { networkChart } from '../../../../services/chart';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
stability: 'stable',
|
|
||||||
|
|
||||||
desc: {
|
desc: {
|
||||||
'ja-JP': 'ネットワークのチャートを取得します。'
|
'ja-JP': 'ネットワークのチャートを取得します。'
|
||||||
},
|
},
|
||||||
|
@ -4,8 +4,6 @@ import { convertLog } from '../../../../services/chart/core';
|
|||||||
import { notesChart } from '../../../../services/chart';
|
import { notesChart } from '../../../../services/chart';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
stability: 'stable',
|
|
||||||
|
|
||||||
desc: {
|
desc: {
|
||||||
'ja-JP': '投稿のチャートを取得します。'
|
'ja-JP': '投稿のチャートを取得します。'
|
||||||
},
|
},
|
||||||
|
@ -5,8 +5,6 @@ import { convertLog } from '../../../../../services/chart/core';
|
|||||||
import { perUserDriveChart } from '../../../../../services/chart';
|
import { perUserDriveChart } from '../../../../../services/chart';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
stability: 'stable',
|
|
||||||
|
|
||||||
desc: {
|
desc: {
|
||||||
'ja-JP': 'ユーザーごとのドライブのチャートを取得します。'
|
'ja-JP': 'ユーザーごとのドライブのチャートを取得します。'
|
||||||
},
|
},
|
||||||
|
@ -5,8 +5,6 @@ import { convertLog } from '../../../../../services/chart/core';
|
|||||||
import { perUserFollowingChart } from '../../../../../services/chart';
|
import { perUserFollowingChart } from '../../../../../services/chart';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
stability: 'stable',
|
|
||||||
|
|
||||||
desc: {
|
desc: {
|
||||||
'ja-JP': 'ユーザーごとのフォロー/フォロワーのチャートを取得します。'
|
'ja-JP': 'ユーザーごとのフォロー/フォロワーのチャートを取得します。'
|
||||||
},
|
},
|
||||||
|
@ -5,8 +5,6 @@ import { convertLog } from '../../../../../services/chart/core';
|
|||||||
import { perUserNotesChart } from '../../../../../services/chart';
|
import { perUserNotesChart } from '../../../../../services/chart';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
stability: 'stable',
|
|
||||||
|
|
||||||
desc: {
|
desc: {
|
||||||
'ja-JP': 'ユーザーごとの投稿のチャートを取得します。'
|
'ja-JP': 'ユーザーごとの投稿のチャートを取得します。'
|
||||||
},
|
},
|
||||||
|
@ -5,8 +5,6 @@ import { convertLog } from '../../../../../services/chart/core';
|
|||||||
import { perUserReactionsChart } from '../../../../../services/chart';
|
import { perUserReactionsChart } from '../../../../../services/chart';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
stability: 'stable',
|
|
||||||
|
|
||||||
desc: {
|
desc: {
|
||||||
'ja-JP': 'ユーザーごとの被リアクション数のチャートを取得します。'
|
'ja-JP': 'ユーザーごとの被リアクション数のチャートを取得します。'
|
||||||
},
|
},
|
||||||
|
@ -4,8 +4,6 @@ import { convertLog } from '../../../../services/chart/core';
|
|||||||
import { usersChart } from '../../../../services/chart';
|
import { usersChart } from '../../../../services/chart';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
stability: 'stable',
|
|
||||||
|
|
||||||
desc: {
|
desc: {
|
||||||
'ja-JP': 'ユーザーのチャートを取得します。'
|
'ja-JP': 'ユーザーのチャートを取得します。'
|
||||||
},
|
},
|
||||||
|
@ -5,8 +5,6 @@ import { ApiError } from '../../../error';
|
|||||||
import { DriveFiles, Notes } from '../../../../../models';
|
import { DriveFiles, Notes } from '../../../../../models';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
stability: 'stable',
|
|
||||||
|
|
||||||
desc: {
|
desc: {
|
||||||
'ja-JP': '指定したドライブのファイルが添付されている投稿一覧を取得します。',
|
'ja-JP': '指定したドライブのファイルが添付されている投稿一覧を取得します。',
|
||||||
'en-US': 'Get the notes that specified file of drive attached.'
|
'en-US': 'Get the notes that specified file of drive attached.'
|
||||||
|
@ -7,8 +7,6 @@ import { ApiError } from '../../../error';
|
|||||||
import { DriveFiles } from '../../../../../models';
|
import { DriveFiles } from '../../../../../models';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
stability: 'stable',
|
|
||||||
|
|
||||||
desc: {
|
desc: {
|
||||||
'ja-JP': 'ドライブのファイルを削除します。',
|
'ja-JP': 'ドライブのファイルを削除します。',
|
||||||
'en-US': 'Delete a file of drive.'
|
'en-US': 'Delete a file of drive.'
|
||||||
|
@ -6,8 +6,6 @@ import { DriveFile } from '../../../../../models/entities/drive-file';
|
|||||||
import { DriveFiles } from '../../../../../models';
|
import { DriveFiles } from '../../../../../models';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
stability: 'stable',
|
|
||||||
|
|
||||||
desc: {
|
desc: {
|
||||||
'ja-JP': '指定したドライブのファイルの情報を取得します。',
|
'ja-JP': '指定したドライブのファイルの情報を取得します。',
|
||||||
'en-US': 'Get specified file of drive.'
|
'en-US': 'Get specified file of drive.'
|
||||||
|
@ -7,8 +7,6 @@ import { DriveFolders } from '../../../../../models';
|
|||||||
import { genId } from '../../../../../misc/gen-id';
|
import { genId } from '../../../../../misc/gen-id';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
stability: 'stable',
|
|
||||||
|
|
||||||
desc: {
|
desc: {
|
||||||
'ja-JP': 'ドライブのフォルダを作成します。',
|
'ja-JP': 'ドライブのフォルダを作成します。',
|
||||||
'en-US': 'Create a folder of drive.'
|
'en-US': 'Create a folder of drive.'
|
||||||
|
@ -6,8 +6,6 @@ import { ApiError } from '../../../error';
|
|||||||
import { DriveFolders, DriveFiles } from '../../../../../models';
|
import { DriveFolders, DriveFiles } from '../../../../../models';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
stability: 'stable',
|
|
||||||
|
|
||||||
desc: {
|
desc: {
|
||||||
'ja-JP': '指定したドライブのフォルダを削除します。',
|
'ja-JP': '指定したドライブのフォルダを削除します。',
|
||||||
'en-US': 'Delete specified folder of drive.'
|
'en-US': 'Delete specified folder of drive.'
|
||||||
|
@ -5,8 +5,6 @@ import { ApiError } from '../../../error';
|
|||||||
import { DriveFolders } from '../../../../../models';
|
import { DriveFolders } from '../../../../../models';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
stability: 'stable',
|
|
||||||
|
|
||||||
desc: {
|
desc: {
|
||||||
'ja-JP': '指定したドライブのフォルダの情報を取得します。',
|
'ja-JP': '指定したドライブのフォルダの情報を取得します。',
|
||||||
'en-US': 'Get specified folder of drive.'
|
'en-US': 'Get specified folder of drive.'
|
||||||
|
@ -6,8 +6,6 @@ import { ApiError } from '../../../error';
|
|||||||
import { DriveFolders } from '../../../../../models';
|
import { DriveFolders } from '../../../../../models';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
stability: 'stable',
|
|
||||||
|
|
||||||
desc: {
|
desc: {
|
||||||
'ja-JP': '指定したドライブのフォルダの情報を更新します。',
|
'ja-JP': '指定したドライブのフォルダの情報を更新します。',
|
||||||
'en-US': 'Update specified folder of drive.'
|
'en-US': 'Update specified folder of drive.'
|
||||||
|
@ -8,8 +8,6 @@ import { getUser } from '../../common/getters';
|
|||||||
import { Followings, Users } from '../../../../models';
|
import { Followings, Users } from '../../../../models';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
stability: 'stable',
|
|
||||||
|
|
||||||
desc: {
|
desc: {
|
||||||
'ja-JP': '指定したユーザーをフォローします。',
|
'ja-JP': '指定したユーザーをフォローします。',
|
||||||
'en-US': 'Follow a user.'
|
'en-US': 'Follow a user.'
|
||||||
|
@ -8,8 +8,6 @@ import { getUser } from '../../common/getters';
|
|||||||
import { Followings, Users } from '../../../../models';
|
import { Followings, Users } from '../../../../models';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
stability: 'stable',
|
|
||||||
|
|
||||||
desc: {
|
desc: {
|
||||||
'ja-JP': '指定したユーザーのフォローを解除します。',
|
'ja-JP': '指定したユーザーのフォローを解除します。',
|
||||||
'en-US': 'Unfollow a user.'
|
'en-US': 'Unfollow a user.'
|
||||||
|
@ -2,8 +2,6 @@ import define from '../define';
|
|||||||
import { Users } from '../../../models';
|
import { Users } from '../../../models';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
stability: 'stable',
|
|
||||||
|
|
||||||
desc: {
|
desc: {
|
||||||
'ja-JP': '自分のアカウント情報を取得します。'
|
'ja-JP': '自分のアカウント情報を取得します。'
|
||||||
},
|
},
|
||||||
|
@ -6,8 +6,6 @@ import { ApiError } from '../../error';
|
|||||||
import { Users } from '../../../../models';
|
import { Users } from '../../../../models';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
stability: 'stable',
|
|
||||||
|
|
||||||
desc: {
|
desc: {
|
||||||
'ja-JP': '指定した投稿をピン留めします。'
|
'ja-JP': '指定した投稿をピン留めします。'
|
||||||
},
|
},
|
||||||
|
@ -6,8 +6,6 @@ import { ApiError } from '../../error';
|
|||||||
import { Users } from '../../../../models';
|
import { Users } from '../../../../models';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
stability: 'stable',
|
|
||||||
|
|
||||||
desc: {
|
desc: {
|
||||||
'ja-JP': '指定した投稿のピン留めを解除します。'
|
'ja-JP': '指定した投稿のピン留めを解除します。'
|
||||||
},
|
},
|
||||||
|
@ -126,6 +126,10 @@ export const meta = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
injectFeaturedNote: {
|
||||||
|
validator: $.optional.bool,
|
||||||
|
},
|
||||||
|
|
||||||
alwaysMarkNsfw: {
|
alwaysMarkNsfw: {
|
||||||
validator: $.optional.bool,
|
validator: $.optional.bool,
|
||||||
desc: {
|
desc: {
|
||||||
@ -195,6 +199,7 @@ export default define(meta, async (ps, user, app) => {
|
|||||||
if (typeof ps.autoAcceptFollowed == 'boolean') profileUpdates.autoAcceptFollowed = ps.autoAcceptFollowed;
|
if (typeof ps.autoAcceptFollowed == 'boolean') profileUpdates.autoAcceptFollowed = ps.autoAcceptFollowed;
|
||||||
if (typeof ps.isCat == 'boolean') updates.isCat = ps.isCat;
|
if (typeof ps.isCat == 'boolean') updates.isCat = ps.isCat;
|
||||||
if (typeof ps.autoWatch == 'boolean') profileUpdates.autoWatch = ps.autoWatch;
|
if (typeof ps.autoWatch == 'boolean') profileUpdates.autoWatch = ps.autoWatch;
|
||||||
|
if (typeof ps.injectFeaturedNote == 'boolean') profileUpdates.injectFeaturedNote = ps.injectFeaturedNote;
|
||||||
if (typeof ps.alwaysMarkNsfw == 'boolean') profileUpdates.alwaysMarkNsfw = ps.alwaysMarkNsfw;
|
if (typeof ps.alwaysMarkNsfw == 'boolean') profileUpdates.alwaysMarkNsfw = ps.alwaysMarkNsfw;
|
||||||
|
|
||||||
if (ps.avatarId) {
|
if (ps.avatarId) {
|
||||||
|
@ -7,8 +7,6 @@ import { ApiError } from '../../../error';
|
|||||||
import { MessagingMessages } from '../../../../../models';
|
import { MessagingMessages } from '../../../../../models';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
stability: 'stable',
|
|
||||||
|
|
||||||
desc: {
|
desc: {
|
||||||
'ja-JP': '指定したトークメッセージを削除します。',
|
'ja-JP': '指定したトークメッセージを削除します。',
|
||||||
'en-US': 'Delete a message.'
|
'en-US': 'Delete a message.'
|
||||||
|
@ -6,8 +6,6 @@ import { Emojis, Users } from '../../../models';
|
|||||||
import { DB_MAX_NOTE_TEXT_LENGTH } from '../../../misc/hard-limits';
|
import { DB_MAX_NOTE_TEXT_LENGTH } from '../../../misc/hard-limits';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
stability: 'stable',
|
|
||||||
|
|
||||||
desc: {
|
desc: {
|
||||||
'ja-JP': 'インスタンス情報を取得します。',
|
'ja-JP': 'インスタンス情報を取得します。',
|
||||||
'en-US': 'Get the information of this instance.'
|
'en-US': 'Get the information of this instance.'
|
||||||
|
@ -21,8 +21,6 @@ setInterval(() => {
|
|||||||
}, 3000);
|
}, 3000);
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
stability: 'stable',
|
|
||||||
|
|
||||||
desc: {
|
desc: {
|
||||||
'ja-JP': '投稿します。'
|
'ja-JP': '投稿します。'
|
||||||
},
|
},
|
||||||
|
@ -9,8 +9,6 @@ import { Users } from '../../../../models';
|
|||||||
import { ensure } from '../../../../prelude/ensure';
|
import { ensure } from '../../../../prelude/ensure';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
stability: 'stable',
|
|
||||||
|
|
||||||
desc: {
|
desc: {
|
||||||
'ja-JP': '指定した投稿を削除します。',
|
'ja-JP': '指定した投稿を削除します。',
|
||||||
'en-US': 'Delete a note.'
|
'en-US': 'Delete a note.'
|
||||||
|
@ -7,8 +7,6 @@ import { NoteFavorites } from '../../../../../models';
|
|||||||
import { genId } from '../../../../../misc/gen-id';
|
import { genId } from '../../../../../misc/gen-id';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
stability: 'stable',
|
|
||||||
|
|
||||||
desc: {
|
desc: {
|
||||||
'ja-JP': '指定した投稿をお気に入りに登録します。',
|
'ja-JP': '指定した投稿をお気に入りに登録します。',
|
||||||
'en-US': 'Favorite a note.'
|
'en-US': 'Favorite a note.'
|
||||||
|
@ -6,8 +6,6 @@ import { getNote } from '../../../common/getters';
|
|||||||
import { NoteFavorites } from '../../../../../models';
|
import { NoteFavorites } from '../../../../../models';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
stability: 'stable',
|
|
||||||
|
|
||||||
desc: {
|
desc: {
|
||||||
'ja-JP': '指定した投稿のお気に入りを解除します。',
|
'ja-JP': '指定した投稿のお気に入りを解除します。',
|
||||||
'en-US': 'Unfavorite a note.'
|
'en-US': 'Unfavorite a note.'
|
||||||
|
@ -46,6 +46,7 @@ export default define(meta, async (ps, user) => {
|
|||||||
const query = Notes.createQueryBuilder('note')
|
const query = Notes.createQueryBuilder('note')
|
||||||
.addSelect('note.score')
|
.addSelect('note.score')
|
||||||
.where('note.userHost IS NULL')
|
.where('note.userHost IS NULL')
|
||||||
|
.andWhere(`note.score > 0`)
|
||||||
.andWhere(`note.createdAt > :date`, { date: new Date(Date.now() - day) })
|
.andWhere(`note.createdAt > :date`, { date: new Date(Date.now() - day) })
|
||||||
.andWhere(`note.visibility = 'public'`)
|
.andWhere(`note.visibility = 'public'`)
|
||||||
.leftJoinAndSelect('note.user', 'user');
|
.leftJoinAndSelect('note.user', 'user');
|
||||||
|
@ -7,8 +7,9 @@ import { makePaginationQuery } from '../../common/make-pagination-query';
|
|||||||
import { Notes } from '../../../../models';
|
import { Notes } from '../../../../models';
|
||||||
import { generateMuteQuery } from '../../common/generate-mute-query';
|
import { generateMuteQuery } from '../../common/generate-mute-query';
|
||||||
import { activeUsersChart } from '../../../../services/chart';
|
import { activeUsersChart } from '../../../../services/chart';
|
||||||
import { Brackets } from 'typeorm';
|
|
||||||
import { generateRepliesQuery } from '../../common/generate-replies-query';
|
import { generateRepliesQuery } from '../../common/generate-replies-query';
|
||||||
|
import { injectPromo } from '../../common/inject-promo';
|
||||||
|
import { injectFeatured } from '../../common/inject-featured';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
desc: {
|
desc: {
|
||||||
@ -90,6 +91,9 @@ export default define(meta, async (ps, user) => {
|
|||||||
|
|
||||||
const timeline = await query.take(ps.limit!).getMany();
|
const timeline = await query.take(ps.limit!).getMany();
|
||||||
|
|
||||||
|
await injectPromo(timeline, user);
|
||||||
|
await injectFeatured(timeline, user);
|
||||||
|
|
||||||
process.nextTick(() => {
|
process.nextTick(() => {
|
||||||
if (user) {
|
if (user) {
|
||||||
activeUsersChart.update(user);
|
activeUsersChart.update(user);
|
||||||
|
@ -10,6 +10,8 @@ import { generateVisibilityQuery } from '../../common/generate-visibility-query'
|
|||||||
import { generateMuteQuery } from '../../common/generate-mute-query';
|
import { generateMuteQuery } from '../../common/generate-mute-query';
|
||||||
import { activeUsersChart } from '../../../../services/chart';
|
import { activeUsersChart } from '../../../../services/chart';
|
||||||
import { generateRepliesQuery } from '../../common/generate-replies-query';
|
import { generateRepliesQuery } from '../../common/generate-replies-query';
|
||||||
|
import { injectPromo } from '../../common/inject-promo';
|
||||||
|
import { injectFeatured } from '../../common/inject-featured';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
desc: {
|
desc: {
|
||||||
@ -169,6 +171,9 @@ export default define(meta, async (ps, user) => {
|
|||||||
|
|
||||||
const timeline = await query.take(ps.limit!).getMany();
|
const timeline = await query.take(ps.limit!).getMany();
|
||||||
|
|
||||||
|
await injectPromo(timeline, user);
|
||||||
|
await injectFeatured(timeline, user);
|
||||||
|
|
||||||
process.nextTick(() => {
|
process.nextTick(() => {
|
||||||
if (user) {
|
if (user) {
|
||||||
activeUsersChart.update(user);
|
activeUsersChart.update(user);
|
||||||
|
@ -10,6 +10,8 @@ import { generateVisibilityQuery } from '../../common/generate-visibility-query'
|
|||||||
import { activeUsersChart } from '../../../../services/chart';
|
import { activeUsersChart } from '../../../../services/chart';
|
||||||
import { Brackets } from 'typeorm';
|
import { Brackets } from 'typeorm';
|
||||||
import { generateRepliesQuery } from '../../common/generate-replies-query';
|
import { generateRepliesQuery } from '../../common/generate-replies-query';
|
||||||
|
import { injectPromo } from '../../common/inject-promo';
|
||||||
|
import { injectFeatured } from '../../common/inject-featured';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
desc: {
|
desc: {
|
||||||
@ -122,6 +124,9 @@ export default define(meta, async (ps, user) => {
|
|||||||
|
|
||||||
const timeline = await query.take(ps.limit!).getMany();
|
const timeline = await query.take(ps.limit!).getMany();
|
||||||
|
|
||||||
|
await injectPromo(timeline, user);
|
||||||
|
await injectFeatured(timeline, user);
|
||||||
|
|
||||||
process.nextTick(() => {
|
process.nextTick(() => {
|
||||||
if (user) {
|
if (user) {
|
||||||
activeUsersChart.update(user);
|
activeUsersChart.update(user);
|
||||||
|
@ -6,8 +6,6 @@ import { getNote } from '../../../common/getters';
|
|||||||
import { ApiError } from '../../../error';
|
import { ApiError } from '../../../error';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
stability: 'stable',
|
|
||||||
|
|
||||||
desc: {
|
desc: {
|
||||||
'ja-JP': '指定した投稿にリアクションします。',
|
'ja-JP': '指定した投稿にリアクションします。',
|
||||||
'en-US': 'React to a note.'
|
'en-US': 'React to a note.'
|
||||||
|
@ -6,8 +6,6 @@ import { ApiError } from '../../error';
|
|||||||
import { Notes } from '../../../../models';
|
import { Notes } from '../../../../models';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
stability: 'stable',
|
|
||||||
|
|
||||||
desc: {
|
desc: {
|
||||||
'ja-JP': '指定した投稿を取得します。',
|
'ja-JP': '指定した投稿を取得します。',
|
||||||
'en-US': 'Get a note.'
|
'en-US': 'Get a note.'
|
||||||
|
@ -4,8 +4,6 @@ import define from '../../define';
|
|||||||
import { NoteFavorites, NoteWatchings } from '../../../../models';
|
import { NoteFavorites, NoteWatchings } from '../../../../models';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
stability: 'stable',
|
|
||||||
|
|
||||||
desc: {
|
desc: {
|
||||||
'ja-JP': '指定した投稿の状態を取得します。',
|
'ja-JP': '指定した投稿の状態を取得します。',
|
||||||
'en-US': 'Get state of a note.'
|
'en-US': 'Get state of a note.'
|
||||||
|
@ -8,6 +8,8 @@ import { generateMuteQuery } from '../../common/generate-mute-query';
|
|||||||
import { activeUsersChart } from '../../../../services/chart';
|
import { activeUsersChart } from '../../../../services/chart';
|
||||||
import { Brackets } from 'typeorm';
|
import { Brackets } from 'typeorm';
|
||||||
import { generateRepliesQuery } from '../../common/generate-replies-query';
|
import { generateRepliesQuery } from '../../common/generate-replies-query';
|
||||||
|
import { injectPromo } from '../../common/inject-promo';
|
||||||
|
import { injectFeatured } from '../../common/inject-featured';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
desc: {
|
desc: {
|
||||||
@ -155,6 +157,9 @@ export default define(meta, async (ps, user) => {
|
|||||||
|
|
||||||
const timeline = await query.take(ps.limit!).getMany();
|
const timeline = await query.take(ps.limit!).getMany();
|
||||||
|
|
||||||
|
await injectPromo(timeline, user);
|
||||||
|
await injectFeatured(timeline, user);
|
||||||
|
|
||||||
process.nextTick(() => {
|
process.nextTick(() => {
|
||||||
if (user) {
|
if (user) {
|
||||||
activeUsersChart.update(user);
|
activeUsersChart.update(user);
|
||||||
|
@ -6,8 +6,6 @@ import { getNote } from '../../../common/getters';
|
|||||||
import { ApiError } from '../../../error';
|
import { ApiError } from '../../../error';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
stability: 'stable',
|
|
||||||
|
|
||||||
desc: {
|
desc: {
|
||||||
'ja-JP': '指定した投稿をウォッチします。',
|
'ja-JP': '指定した投稿をウォッチします。',
|
||||||
'en-US': 'Watch a note.'
|
'en-US': 'Watch a note.'
|
||||||
|
@ -6,8 +6,6 @@ import { getNote } from '../../../common/getters';
|
|||||||
import { ApiError } from '../../../error';
|
import { ApiError } from '../../../error';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
stability: 'stable',
|
|
||||||
|
|
||||||
desc: {
|
desc: {
|
||||||
'ja-JP': '指定した投稿のウォッチを解除します。',
|
'ja-JP': '指定した投稿のウォッチを解除します。',
|
||||||
'en-US': 'Unwatch a note.'
|
'en-US': 'Unwatch a note.'
|
||||||
|
50
src/server/api/endpoints/promo/read.ts
Normal file
50
src/server/api/endpoints/promo/read.ts
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import $ from 'cafy';
|
||||||
|
import { ID } from '../../../../misc/cafy-id';
|
||||||
|
import define from '../../define';
|
||||||
|
import { ApiError } from '../../error';
|
||||||
|
import { getNote } from '../../common/getters';
|
||||||
|
import { PromoReads } from '../../../../models';
|
||||||
|
import { genId } from '../../../../misc/gen-id';
|
||||||
|
|
||||||
|
export const meta = {
|
||||||
|
tags: ['notes'],
|
||||||
|
|
||||||
|
requireCredential: true as const,
|
||||||
|
|
||||||
|
params: {
|
||||||
|
noteId: {
|
||||||
|
validator: $.type(ID),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
errors: {
|
||||||
|
noSuchNote: {
|
||||||
|
message: 'No such note.',
|
||||||
|
code: 'NO_SUCH_NOTE',
|
||||||
|
id: 'd785b897-fcd3-4fe9-8fc3-b85c26e6c932'
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default define(meta, async (ps, user) => {
|
||||||
|
const note = await getNote(ps.noteId).catch(e => {
|
||||||
|
if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
|
||||||
|
throw e;
|
||||||
|
});
|
||||||
|
|
||||||
|
const exist = await PromoReads.findOne({
|
||||||
|
noteId: note.id,
|
||||||
|
userId: user.id
|
||||||
|
});
|
||||||
|
|
||||||
|
if (exist != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await PromoReads.save({
|
||||||
|
id: genId(),
|
||||||
|
createdAt: new Date(),
|
||||||
|
noteId: note.id,
|
||||||
|
userId: user.id
|
||||||
|
});
|
||||||
|
});
|
@ -275,12 +275,12 @@ export default abstract class Chart<T extends Record<string, any>> {
|
|||||||
data = this.getNewLog(obj);
|
data = this.getNewLog(obj);
|
||||||
} else {
|
} else {
|
||||||
// ログが存在しなかったら
|
// ログが存在しなかったら
|
||||||
// (Misskeyインスタンスを建てて初めてのチャート更新時)
|
// (Misskeyインスタンスを建てて初めてのチャート更新時など)
|
||||||
|
|
||||||
// 初期ログデータを作成
|
// 初期ログデータを作成
|
||||||
data = this.getNewLog(null);
|
data = this.getNewLog(null);
|
||||||
|
|
||||||
logger.info(`${this.name}: Initial commit created`);
|
logger.info(`${this.name + (group ? `:${group}` : '')} (${span}): Initial commit created`);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -292,14 +292,14 @@ export default abstract class Chart<T extends Record<string, any>> {
|
|||||||
...Chart.convertObjectToFlattenColumns(data)
|
...Chart.convertObjectToFlattenColumns(data)
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.info(`${this.name}: New commit created`);
|
logger.info(`${this.name + (group ? `:${group}` : '')} (${span}): New commit created`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// duplicate key error
|
// duplicate key error
|
||||||
// 並列動作している他のチャートエンジンプロセスと処理が重なる場合がある
|
// 並列動作している他のチャートエンジンプロセスと処理が重なる場合がある
|
||||||
// その場合は再度最も新しいログを持ってくる
|
// その場合は再度最も新しいログを持ってくる
|
||||||
if (isDuplicateKeyValueError(e)) {
|
if (isDuplicateKeyValueError(e)) {
|
||||||
log = await this.getLatestLog(span, group) as Log;
|
log = await this.getLatestLog(span, group) as Log;
|
||||||
logger.info(`${this.name}: Commit duplicated`);
|
logger.info(`${this.name + (group ? `:${group}` : '')} (${span}): Commit duplicated`);
|
||||||
} else {
|
} else {
|
||||||
logger.error(e);
|
logger.error(e);
|
||||||
throw e;
|
throw e;
|
||||||
|
Reference in New Issue
Block a user