Compare commits

...

18 Commits

Author SHA1 Message Date
50d1500dfc 12.13.0 2020-02-18 19:50:04 +09:00
94441f93a5 New Crowdin translations (#5969)
* New translations ja-JP.yml (Kannada)

* New translations ja-JP.yml (Kannada)

* New translations ja-JP.yml (Kannada)

* New translations ja-JP.yml (Kannada)

* New translations ja-JP.yml (Chinese Simplified)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Spanish)
2020-02-18 19:48:14 +09:00
5f712fbf3c Implement photo widget 2020-02-18 19:47:30 +09:00
1c757f10e0 Update CHANGELOG.md 2020-02-18 19:36:20 +09:00
0508d5f643 Add activity widget 2020-02-18 19:31:11 +09:00
d9986b7a2f Implement featured note injection 2020-02-18 19:05:11 +09:00
3d79e7a136 Improve paging 2020-02-18 18:19:11 +09:00
52fb1237ec Imprement promo read 2020-02-18 18:14:38 +09:00
8a7197726e Merge branch 'develop' of https://github.com/syuilo/misskey into develop 2020-02-18 17:54:13 +09:00
b7f5458684 Fix bug 2020-02-18 17:53:56 +09:00
52710f3810 管理者はモデレーターに変更できないように (#5970)
* 管理者をモデレーターに変更できないように

* Change error message
2020-02-18 17:53:52 +09:00
a54de07260 Resolve #5963 2020-02-18 08:41:32 +09:00
aa2c8d101e Fix type 2020-02-18 08:13:47 +09:00
1441fd93b9 Clean up 2020-02-18 08:05:27 +09:00
4a585e8920 Improve chart logging 2020-02-18 03:03:34 +09:00
8c4245a09d Update core.ts 2020-02-18 02:27:18 +09:00
e4af16989a Fix bug 2020-02-18 01:25:02 +09:00
5dc0944fe8 Resolve #5949 2020-02-18 01:12:35 +09:00
84 changed files with 891 additions and 102 deletions

View File

@ -1,6 +1,25 @@
ChangeLog
=========
12.13.0 (2020/02/18)
-------------------
### ✨Improvements
* プロモーションノート機能を実装
* インスタンス管理者が、重要なお知らせやユーザーにやってもらいたいアンケートなどをタイムラインの途中に挿入する機能
* プロモーションされる期限を設定できる
* 複数のプロモーションがある場合はランダムに選択されて表示される
* ユーザーがプロモーションを個別に非表示にすることもできる
* ハイライトインジェクション機能を実装
* タイムラインの途中におすすめのノートを表示できる機能
* 設定で有効/無効を切り替えられる
* アクティビティウィジェットを実装
* フォトウィジェットを実装
* タイムラインの一番上までスクロールできるように
* 管理者はモデレーターに変更できないように
### 🐛Fixes
* admin/show-users APIがadminかつmoderator設定されているとき使えない問題を修正
12.12.0 (2020/02/17)
-------------------
### ✨Improvements

View File

@ -512,6 +512,7 @@ _widgets:
trends: "Trending"
clock: "Clock"
rss: "RSS reader"
activity: "Activity"
_cw:
hide: "Hide"
show: "Load more"

View File

@ -512,6 +512,7 @@ _widgets:
trends: "Tendencias"
clock: "Reloj"
rss: "Lector RSS"
activity: "Actividad"
_cw:
hide: "Ocultar"
show: "Ver más"

View File

@ -492,6 +492,7 @@ _widgets:
trends: "Tendances"
clock: "Horloge"
rss: "Lecteur de flux RSS"
activity: "Activités"
_cw:
hide: "Masquer"
show: "Voir plus"

View File

@ -412,6 +412,11 @@ dayOverDayChanges: "前日比"
accessibility: "アクセシビリティ"
clinetSettings: "クライアント設定"
accountSettings: "アカウント設定"
promotion: "プロモーション"
promote: "プロモート"
numberOfDays: "日数"
hideThisNote: "このノートを非表示"
showFeaturedNotesInTimeline: "タイムラインにおすすめのノートを表示する"
_ago:
unknown: "謎"
@ -521,6 +526,8 @@ _widgets:
trends: "トレンド"
clock: "時計"
rss: "RSSリーダー"
activity: "アクティビティ"
photos: "フォト"
_cw:
hide: "隠す"

View File

@ -1,2 +1,31 @@
---
_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: "ನೋಂದಣಿ"
instances: "ನಿದರ್ಶನ"
_widgets:
notifications: "ಅಧಿಸೂಚನೆಗಳು"
timeline: "ಸಮಯಸಾಲು"
_profile:
username: "ಬಳಕೆಹೆಸರು"

View File

@ -512,6 +512,7 @@ _widgets:
trends: "트렌드"
clock: "시계"
rss: "RSS 리더"
activity: "활동"
_cw:
hide: "숨기기"
show: "더 보기"

View File

@ -463,6 +463,7 @@ _widgets:
trends: "趋势"
clock: "时钟"
rss: "RSS阅读器"
activity: "活动"
_cw:
hide: "隐藏"
show: "查看更多"

View 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);
}
}

View 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);
}
}

View File

@ -1,7 +1,7 @@
{
"name": "misskey",
"author": "syuilo <syuilotan@yahoo.co.jp>",
"version": "12.12.0",
"version": "12.13.0",
"codename": "indigo",
"repository": {
"type": "git",

View File

@ -44,11 +44,11 @@
<mk-avatar :user="$store.state.i" class="avatar"/><mk-acct class="text" :user="$store.state.i"/>
</button>
<div class="divider"></div>
<router-link class="item index" active-class="active" to="/" exact v-if="$store.getters.isSignedIn">
<fa :icon="faHome" fixed-width/><span class="text">{{ $t('timeline') }}</span>
</router-link>
<button class="item _button index active" @click="top()" v-if="$route.name === 'index'">
<fa :icon="faHome" fixed-width/><span class="text">{{ $store.getters.isSignedIn ? $t('timeline') : $t('home') }}</span>
</button>
<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>
<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>
@ -140,8 +140,9 @@
</div>
<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 v-if="$store.getters.isSignedIn" class="button home _button" :disabled="$route.path === '/'" @click="$router.push('/')"><fa :icon="faHome"/></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="$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 post _buttonPrimary" @click="post()"><fa :icon="faPencilAlt"/></button>
</div>
@ -321,6 +322,10 @@ export default Vue.extend({
setTimeout(adjust, 100);
},
top() {
window.scroll({ top: 0, behavior: 'smooth' });
},
help() {
this.$router.push('/docs/keyboard-shortcut');
},
@ -601,7 +606,9 @@ export default Vue.extend({
'calendar',
'rss',
'trends',
'clock'
'clock',
'activity',
'photos',
];
this.$root.menu({

View File

@ -2,7 +2,7 @@
<sequential-entrance class="sqadhkmv" ref="list" :direction="direction" :reversed="reversed">
<template v-for="(item, i) in items">
<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">
<span><fa class="icon" :icon="faAngleUp"/>{{ getDateText(item.createdAt) }}</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() {
this.$refs.list.focus();
}

View File

@ -9,7 +9,9 @@
>
<x-sub v-for="note in conversation" :key="note.id" :note="note"/>
<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">
<mk-avatar class="avatar" :user="note.user"/>
<fa :icon="faRetweet"/>
@ -83,7 +85,7 @@
<script lang="ts">
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 { parse } from '../../mfm/parse';
import { sum, unique } from '../../prelude/array';
@ -140,7 +142,7 @@ export default Vue.extend({
replies: [],
showContent: 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: {
readPromo() {
(this as any).$root.api('promo/read', {
noteId: this.appearNote.id
});
this.hideThisNote = true;
},
capture(withHandler = false) {
if (this.$store.getters.isSignedIn) {
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'),
action: () => this.togglePin(true)
} : 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 ? [
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() {
this.$el.focus();
},
@ -710,7 +752,9 @@ export default Vue.extend({
border-radius: 0 0 var(--radius) var(--radius);
}
> .pinned {
> .info {
display: flex;
align-items: center;
padding: 16px 32px 8px 32px;
line-height: 24px;
font-size: 90%;
@ -724,9 +768,14 @@ export default Vue.extend({
> [data-icon] {
margin-right: 4px;
}
> .hide {
margin-left: auto;
color: inherit;
}
}
> .pinned + .article {
> .info + .article {
padding-top: 8px;
}

View File

@ -15,7 +15,7 @@
</div>
<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>
<div class="more" v-if="more && !reversed" style="margin-top: var(--margin);">

View File

@ -3,7 +3,7 @@
<template #header><mk-user-name :user="user"/></template>
<div class="vrcsvlkm">
<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="toggleSuspend()" v-model="suspended">{{ $t('suspend') }}</mk-switch>
</div>
@ -47,7 +47,7 @@ export default Vue.extend({
type: 'waiting',
iconOnly: true
});
this.$root.api('admin/reset-password', {
userId: this.user.id,
}).then(({ password }) => {

View File

@ -13,6 +13,9 @@
<mk-switch v-model="$store.state.i.autoWatch" @change="onChangeAutoWatch">
{{ $t('autoNoteWatch') }}<template #desc>{{ $t('autoNoteWatchDescription') }}</template>
</mk-switch>
<mk-switch v-model="$store.state.i.injectFeaturedNote" @change="onChangeInjectFeaturedNote">
{{ $t('showFeaturedNotesInTimeline') }}
</mk-switch>
</div>
<div class="_content">
<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() {
this.$root.api('i/read_all_unread_notes');
},

View File

@ -67,7 +67,7 @@ export default (opts) => ({
...params,
limit: this.pagination.noPaging ? (this.pagination.limit || 10) : (this.pagination.limit || 10) + 1,
}).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();
this.items = this.pagination.reversed ? [...items].reverse() : items;
this.more = true;
@ -103,7 +103,7 @@ export default (opts) => ({
untilId: this.items[this.items.length - 1].id,
}),
}).then(items => {
if (items.length === SECOND_FETCH_LIMIT + 1) {
if (items.length > SECOND_FETCH_LIMIT) {
items.pop();
this.items = this.pagination.reversed ? [...items].reverse().concat(this.items) : this.items.concat(items);
this.more = true;

View 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>

View 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>

View 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>

View File

@ -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-trends', () => import('./trends.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));

View 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>

View File

@ -55,6 +55,8 @@ import { Clip } from '../models/entities/clip';
import { ClipNote } from '../models/entities/clip-note';
import { Antenna } from '../models/entities/antenna';
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);
@ -140,6 +142,8 @@ export const entities = [
ClipNote,
Antenna,
AntennaNote,
PromoNote,
PromoRead,
ReversiGame,
ReversiMatching,
...charts as any

View 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
}

View 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;
}

View File

@ -125,6 +125,11 @@ export class UserProfile {
})
public carefulBot: boolean;
@Column('boolean', {
default: true,
})
public injectFeaturedNote: boolean;
@Column({
...id(),
nullable: true

View File

@ -50,6 +50,8 @@ import { ClipRepository } from './repositories/clip';
import { ClipNote } from './entities/clip-note';
import { AntennaRepository } from './repositories/antenna';
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 AnnouncementReads = getRepository(AnnouncementRead);
@ -102,3 +104,5 @@ export const Clips = getCustomRepository(ClipRepository);
export const ClipNotes = getRepository(ClipNote);
export const Antennas = getCustomRepository(AntennaRepository);
export const AntennaNotes = getRepository(AntennaNote);
export const PromoNotes = getRepository(PromoNote);
export const PromoReads = getRepository(PromoRead);

View File

@ -196,6 +196,8 @@ export class NoteRepository extends Repository<Note> {
renoteId: note.renoteId,
mentions: note.mentions.length > 0 ? note.mentions : undefined,
uri: note.uri || undefined,
_featuredId_: (note as any)._featuredId_ || undefined,
_prId_: (note as any)._prId_ || undefined,
...(opts.detail ? {
reply: note.replyId ? this.pack(note.replyId, meId, {

View File

@ -227,6 +227,7 @@ export class UserRepository extends Repository<User> {
avatarId: user.avatarId,
bannerId: user.bannerId,
autoWatch: profile!.autoWatch,
injectFeaturedNote: profile!.injectFeaturedNote,
alwaysMarkNsfw: profile!.alwaysMarkNsfw,
carefulBot: profile!.carefulBot,
autoAcceptFollowed: profile!.autoAcceptFollowed,

View File

@ -1,7 +1,7 @@
import { User } from '../../../models/entities/user';
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) {
q.andWhere(new Brackets(qb => { qb
.where(`note.replyId IS NULL`) // 返信ではない

View File

@ -2,7 +2,7 @@ import { User } from '../../../models/entities/user';
import { Followings } from '../../../models';
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) {
q.andWhere(new Brackets(qb => { qb
.where(`note.visibility = 'public'`)

View 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);
}

View 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);
}

View File

@ -88,7 +88,6 @@ export async function signup(username: User['username'], password: UserProfile['
await transactionalEntityManager.save(new UserProfile({
userId: account.id,
autoAcceptFollowed: true,
autoWatch: false,
password: hash,
}));

View File

@ -5,6 +5,7 @@ import { ApiError } from './error';
import { App } from '../../models/entities/app';
import { SchemaType } from '../../misc/schema';
// TODO: defaultが設定されている場合はその型も考慮する
type Params<T extends IEndpointMeta> = {
[P in keyof T['params']]: NonNullable<T['params']>[P]['transform'] extends Function
? ReturnType<NonNullable<T['params']>[P]['transform']>

View File

@ -32,6 +32,10 @@ export default define(meta, async (ps) => {
throw new Error('user not found');
}
if (user.isAdmin) {
throw new Error('cannot mark as moderator if admin user');
}
await Users.update(user.id, {
isModerator: true
});

View 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,
});
});

View File

@ -31,7 +31,7 @@ export default define(meta, async (ps, me) => {
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');
}

View File

@ -8,8 +8,6 @@ import { getUser } from '../../common/getters';
import { Blockings, NoteWatchings, Users } from '../../../../models';
export const meta = {
stability: 'stable',
desc: {
'ja-JP': '指定したユーザーをブロックします。',
'en-US': 'Block a user.'

View File

@ -8,8 +8,6 @@ import { getUser } from '../../common/getters';
import { Blockings, Users } from '../../../../models';
export const meta = {
stability: 'stable',
desc: {
'ja-JP': '指定したユーザーのブロックを解除します。',
'en-US': 'Unblock a user.'

View File

@ -4,8 +4,6 @@ import { convertLog } from '../../../../services/chart/core';
import { activeUsersChart } from '../../../../services/chart';
export const meta = {
stability: 'stable',
desc: {
'ja-JP': 'アクティブユーザーのチャートを取得します。'
},

View File

@ -4,8 +4,6 @@ import { convertLog } from '../../../../services/chart/core';
import { driveChart } from '../../../../services/chart';
export const meta = {
stability: 'stable',
desc: {
'ja-JP': 'ドライブのチャートを取得します。'
},

View File

@ -4,8 +4,6 @@ import { convertLog } from '../../../../services/chart/core';
import { federationChart } from '../../../../services/chart';
export const meta = {
stability: 'stable',
desc: {
'ja-JP': 'フェデレーションのチャートを取得します。'
},

View File

@ -4,8 +4,6 @@ import { convertLog } from '../../../../services/chart/core';
import { hashtagChart } from '../../../../services/chart';
export const meta = {
stability: 'stable',
desc: {
'ja-JP': 'ハッシュタグごとのチャートを取得します。'
},

View File

@ -4,8 +4,6 @@ import { convertLog } from '../../../../services/chart/core';
import { instanceChart } from '../../../../services/chart';
export const meta = {
stability: 'stable',
desc: {
'ja-JP': 'インスタンスごとのチャートを取得します。'
},

View File

@ -4,8 +4,6 @@ import { convertLog } from '../../../../services/chart/core';
import { networkChart } from '../../../../services/chart';
export const meta = {
stability: 'stable',
desc: {
'ja-JP': 'ネットワークのチャートを取得します。'
},

View File

@ -4,8 +4,6 @@ import { convertLog } from '../../../../services/chart/core';
import { notesChart } from '../../../../services/chart';
export const meta = {
stability: 'stable',
desc: {
'ja-JP': '投稿のチャートを取得します。'
},

View File

@ -5,8 +5,6 @@ import { convertLog } from '../../../../../services/chart/core';
import { perUserDriveChart } from '../../../../../services/chart';
export const meta = {
stability: 'stable',
desc: {
'ja-JP': 'ユーザーごとのドライブのチャートを取得します。'
},

View File

@ -5,8 +5,6 @@ import { convertLog } from '../../../../../services/chart/core';
import { perUserFollowingChart } from '../../../../../services/chart';
export const meta = {
stability: 'stable',
desc: {
'ja-JP': 'ユーザーごとのフォロー/フォロワーのチャートを取得します。'
},

View File

@ -5,8 +5,6 @@ import { convertLog } from '../../../../../services/chart/core';
import { perUserNotesChart } from '../../../../../services/chart';
export const meta = {
stability: 'stable',
desc: {
'ja-JP': 'ユーザーごとの投稿のチャートを取得します。'
},

View File

@ -5,8 +5,6 @@ import { convertLog } from '../../../../../services/chart/core';
import { perUserReactionsChart } from '../../../../../services/chart';
export const meta = {
stability: 'stable',
desc: {
'ja-JP': 'ユーザーごとの被リアクション数のチャートを取得します。'
},

View File

@ -4,8 +4,6 @@ import { convertLog } from '../../../../services/chart/core';
import { usersChart } from '../../../../services/chart';
export const meta = {
stability: 'stable',
desc: {
'ja-JP': 'ユーザーのチャートを取得します。'
},

View File

@ -5,8 +5,6 @@ import { ApiError } from '../../../error';
import { DriveFiles, Notes } from '../../../../../models';
export const meta = {
stability: 'stable',
desc: {
'ja-JP': '指定したドライブのファイルが添付されている投稿一覧を取得します。',
'en-US': 'Get the notes that specified file of drive attached.'

View File

@ -7,8 +7,6 @@ import { ApiError } from '../../../error';
import { DriveFiles } from '../../../../../models';
export const meta = {
stability: 'stable',
desc: {
'ja-JP': 'ドライブのファイルを削除します。',
'en-US': 'Delete a file of drive.'

View File

@ -6,8 +6,6 @@ import { DriveFile } from '../../../../../models/entities/drive-file';
import { DriveFiles } from '../../../../../models';
export const meta = {
stability: 'stable',
desc: {
'ja-JP': '指定したドライブのファイルの情報を取得します。',
'en-US': 'Get specified file of drive.'

View File

@ -7,8 +7,6 @@ import { DriveFolders } from '../../../../../models';
import { genId } from '../../../../../misc/gen-id';
export const meta = {
stability: 'stable',
desc: {
'ja-JP': 'ドライブのフォルダを作成します。',
'en-US': 'Create a folder of drive.'

View File

@ -6,8 +6,6 @@ import { ApiError } from '../../../error';
import { DriveFolders, DriveFiles } from '../../../../../models';
export const meta = {
stability: 'stable',
desc: {
'ja-JP': '指定したドライブのフォルダを削除します。',
'en-US': 'Delete specified folder of drive.'

View File

@ -5,8 +5,6 @@ import { ApiError } from '../../../error';
import { DriveFolders } from '../../../../../models';
export const meta = {
stability: 'stable',
desc: {
'ja-JP': '指定したドライブのフォルダの情報を取得します。',
'en-US': 'Get specified folder of drive.'

View File

@ -6,8 +6,6 @@ import { ApiError } from '../../../error';
import { DriveFolders } from '../../../../../models';
export const meta = {
stability: 'stable',
desc: {
'ja-JP': '指定したドライブのフォルダの情報を更新します。',
'en-US': 'Update specified folder of drive.'

View File

@ -8,8 +8,6 @@ import { getUser } from '../../common/getters';
import { Followings, Users } from '../../../../models';
export const meta = {
stability: 'stable',
desc: {
'ja-JP': '指定したユーザーをフォローします。',
'en-US': 'Follow a user.'

View File

@ -8,8 +8,6 @@ import { getUser } from '../../common/getters';
import { Followings, Users } from '../../../../models';
export const meta = {
stability: 'stable',
desc: {
'ja-JP': '指定したユーザーのフォローを解除します。',
'en-US': 'Unfollow a user.'

View File

@ -2,8 +2,6 @@ import define from '../define';
import { Users } from '../../../models';
export const meta = {
stability: 'stable',
desc: {
'ja-JP': '自分のアカウント情報を取得します。'
},

View File

@ -6,8 +6,6 @@ import { ApiError } from '../../error';
import { Users } from '../../../../models';
export const meta = {
stability: 'stable',
desc: {
'ja-JP': '指定した投稿をピン留めします。'
},

View File

@ -6,8 +6,6 @@ import { ApiError } from '../../error';
import { Users } from '../../../../models';
export const meta = {
stability: 'stable',
desc: {
'ja-JP': '指定した投稿のピン留めを解除します。'
},

View File

@ -126,6 +126,10 @@ export const meta = {
}
},
injectFeaturedNote: {
validator: $.optional.bool,
},
alwaysMarkNsfw: {
validator: $.optional.bool,
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.isCat == 'boolean') updates.isCat = ps.isCat;
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 (ps.avatarId) {

View File

@ -7,8 +7,6 @@ import { ApiError } from '../../../error';
import { MessagingMessages } from '../../../../../models';
export const meta = {
stability: 'stable',
desc: {
'ja-JP': '指定したトークメッセージを削除します。',
'en-US': 'Delete a message.'

View File

@ -6,8 +6,6 @@ import { Emojis, Users } from '../../../models';
import { DB_MAX_NOTE_TEXT_LENGTH } from '../../../misc/hard-limits';
export const meta = {
stability: 'stable',
desc: {
'ja-JP': 'インスタンス情報を取得します。',
'en-US': 'Get the information of this instance.'

View File

@ -21,8 +21,6 @@ setInterval(() => {
}, 3000);
export const meta = {
stability: 'stable',
desc: {
'ja-JP': '投稿します。'
},

View File

@ -9,8 +9,6 @@ import { Users } from '../../../../models';
import { ensure } from '../../../../prelude/ensure';
export const meta = {
stability: 'stable',
desc: {
'ja-JP': '指定した投稿を削除します。',
'en-US': 'Delete a note.'

View File

@ -7,8 +7,6 @@ import { NoteFavorites } from '../../../../../models';
import { genId } from '../../../../../misc/gen-id';
export const meta = {
stability: 'stable',
desc: {
'ja-JP': '指定した投稿をお気に入りに登録します。',
'en-US': 'Favorite a note.'

View File

@ -6,8 +6,6 @@ import { getNote } from '../../../common/getters';
import { NoteFavorites } from '../../../../../models';
export const meta = {
stability: 'stable',
desc: {
'ja-JP': '指定した投稿のお気に入りを解除します。',
'en-US': 'Unfavorite a note.'

View File

@ -46,6 +46,7 @@ export default define(meta, async (ps, user) => {
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');

View File

@ -7,8 +7,9 @@ import { makePaginationQuery } from '../../common/make-pagination-query';
import { Notes } from '../../../../models';
import { generateMuteQuery } from '../../common/generate-mute-query';
import { activeUsersChart } from '../../../../services/chart';
import { Brackets } from 'typeorm';
import { generateRepliesQuery } from '../../common/generate-replies-query';
import { injectPromo } from '../../common/inject-promo';
import { injectFeatured } from '../../common/inject-featured';
export const meta = {
desc: {
@ -90,6 +91,9 @@ export default define(meta, async (ps, user) => {
const timeline = await query.take(ps.limit!).getMany();
await injectPromo(timeline, user);
await injectFeatured(timeline, user);
process.nextTick(() => {
if (user) {
activeUsersChart.update(user);

View File

@ -10,6 +10,8 @@ import { generateVisibilityQuery } from '../../common/generate-visibility-query'
import { generateMuteQuery } from '../../common/generate-mute-query';
import { activeUsersChart } from '../../../../services/chart';
import { generateRepliesQuery } from '../../common/generate-replies-query';
import { injectPromo } from '../../common/inject-promo';
import { injectFeatured } from '../../common/inject-featured';
export const meta = {
desc: {
@ -169,6 +171,9 @@ export default define(meta, async (ps, user) => {
const timeline = await query.take(ps.limit!).getMany();
await injectPromo(timeline, user);
await injectFeatured(timeline, user);
process.nextTick(() => {
if (user) {
activeUsersChart.update(user);

View File

@ -10,6 +10,8 @@ import { generateVisibilityQuery } from '../../common/generate-visibility-query'
import { activeUsersChart } from '../../../../services/chart';
import { Brackets } from 'typeorm';
import { generateRepliesQuery } from '../../common/generate-replies-query';
import { injectPromo } from '../../common/inject-promo';
import { injectFeatured } from '../../common/inject-featured';
export const meta = {
desc: {
@ -122,6 +124,9 @@ export default define(meta, async (ps, user) => {
const timeline = await query.take(ps.limit!).getMany();
await injectPromo(timeline, user);
await injectFeatured(timeline, user);
process.nextTick(() => {
if (user) {
activeUsersChart.update(user);

View File

@ -6,8 +6,6 @@ import { getNote } from '../../../common/getters';
import { ApiError } from '../../../error';
export const meta = {
stability: 'stable',
desc: {
'ja-JP': '指定した投稿にリアクションします。',
'en-US': 'React to a note.'

View File

@ -6,8 +6,6 @@ import { ApiError } from '../../error';
import { Notes } from '../../../../models';
export const meta = {
stability: 'stable',
desc: {
'ja-JP': '指定した投稿を取得します。',
'en-US': 'Get a note.'

View File

@ -4,8 +4,6 @@ import define from '../../define';
import { NoteFavorites, NoteWatchings } from '../../../../models';
export const meta = {
stability: 'stable',
desc: {
'ja-JP': '指定した投稿の状態を取得します。',
'en-US': 'Get state of a note.'

View File

@ -8,6 +8,8 @@ import { generateMuteQuery } from '../../common/generate-mute-query';
import { activeUsersChart } from '../../../../services/chart';
import { Brackets } from 'typeorm';
import { generateRepliesQuery } from '../../common/generate-replies-query';
import { injectPromo } from '../../common/inject-promo';
import { injectFeatured } from '../../common/inject-featured';
export const meta = {
desc: {
@ -155,6 +157,9 @@ export default define(meta, async (ps, user) => {
const timeline = await query.take(ps.limit!).getMany();
await injectPromo(timeline, user);
await injectFeatured(timeline, user);
process.nextTick(() => {
if (user) {
activeUsersChart.update(user);

View File

@ -6,8 +6,6 @@ import { getNote } from '../../../common/getters';
import { ApiError } from '../../../error';
export const meta = {
stability: 'stable',
desc: {
'ja-JP': '指定した投稿をウォッチします。',
'en-US': 'Watch a note.'

View File

@ -6,8 +6,6 @@ import { getNote } from '../../../common/getters';
import { ApiError } from '../../../error';
export const meta = {
stability: 'stable',
desc: {
'ja-JP': '指定した投稿のウォッチを解除します。',
'en-US': 'Unwatch a note.'

View 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
});
});

View File

@ -275,12 +275,12 @@ export default abstract class Chart<T extends Record<string, any>> {
data = this.getNewLog(obj);
} else {
// ログが存在しなかったら
// (Misskeyインスタンスを建てて初めてのチャート更新時)
// (Misskeyインスタンスを建てて初めてのチャート更新時など)
// 初期ログデータを作成
data = this.getNewLog(null);
logger.info(`${this.name}: Initial commit created`);
logger.info(`${this.name + (group ? `:${group}` : '')} (${span}): Initial commit created`);
}
try {
@ -292,14 +292,14 @@ export default abstract class Chart<T extends Record<string, any>> {
...Chart.convertObjectToFlattenColumns(data)
});
logger.info(`${this.name}: New commit created`);
logger.info(`${this.name + (group ? `:${group}` : '')} (${span}): New commit created`);
} catch (e) {
// duplicate key error
// 並列動作している他のチャートエンジンプロセスと処理が重なる場合がある
// その場合は再度最も新しいログを持ってくる
if (isDuplicateKeyValueError(e)) {
log = await this.getLatestLog(span, group) as Log;
logger.info(`${this.name}: Commit duplicated`);
logger.info(`${this.name + (group ? `:${group}` : '')} (${span}): Commit duplicated`);
} else {
logger.error(e);
throw e;