summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorsyuilo <Syuilotan@yahoo.co.jp>2020-02-18 19:05:11 +0900
committersyuilo <Syuilotan@yahoo.co.jp>2020-02-18 19:05:11 +0900
commitd9986b7a2fabffff50068f4114a16d315941591f (patch)
tree7627c97a74a4e175cf8e0357cd380d21ac15b898 /src
parentImprove paging (diff)
downloadmisskey-d9986b7a2fabffff50068f4114a16d315941591f.tar.gz
misskey-d9986b7a2fabffff50068f4114a16d315941591f.tar.bz2
misskey-d9986b7a2fabffff50068f4114a16d315941591f.zip
Implement featured note injection
Diffstat (limited to 'src')
-rw-r--r--src/client/components/date-separated-list.vue12
-rw-r--r--src/client/components/note.vue7
-rw-r--r--src/client/components/notes.vue2
-rw-r--r--src/client/pages/my-settings/index.vue9
-rw-r--r--src/models/entities/user-profile.ts5
-rw-r--r--src/models/repositories/note.ts3
-rw-r--r--src/models/repositories/user.ts1
-rw-r--r--src/server/api/common/inject-featured.ts45
-rw-r--r--src/server/api/common/inject-promo.ts10
-rw-r--r--src/server/api/common/signup.ts1
-rw-r--r--src/server/api/define.ts1
-rw-r--r--src/server/api/endpoints/i/update.ts5
-rw-r--r--src/server/api/endpoints/notes/featured.ts1
-rw-r--r--src/server/api/endpoints/notes/global-timeline.ts4
-rw-r--r--src/server/api/endpoints/notes/hybrid-timeline.ts4
-rw-r--r--src/server/api/endpoints/notes/local-timeline.ts4
-rw-r--r--src/server/api/endpoints/notes/timeline.ts4
17 files changed, 102 insertions, 16 deletions
diff --git a/src/client/components/date-separated-list.vue b/src/client/components/date-separated-list.vue
index c425c02dce..5c6917b3f9 100644
--- a/src/client/components/date-separated-list.vue
+++ b/src/client/components/date-separated-list.vue
@@ -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() && !item._prInjectionId_ && !items[i + 1]._prInjectionId_">
+ <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();
}
diff --git a/src/client/components/note.vue b/src/client/components/note.vue
index 3a02753c69..4d325f2806 100644
--- a/src/client/components/note.vue
+++ b/src/client/components/note.vue
@@ -10,7 +10,8 @@
<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="info" v-if="pinned"><fa :icon="faThumbtack"/> {{ $t('pinnedNote') }}</div>
- <div class="info" v-if="appearNote._prInjectionId_"><fa :icon="faBullhorn"/> {{ $t('promotion') }}<button class="_textButton hide" @click="readPromo()">{{ $t('hideThisNote') }}</button></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"/>
@@ -84,7 +85,7 @@
<script lang="ts">
import Vue from 'vue';
-import { faBullhorn, 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';
@@ -141,7 +142,7 @@ export default Vue.extend({
replies: [],
showContent: false,
hideThisNote: false,
- faBullhorn, 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
};
},
diff --git a/src/client/components/notes.vue b/src/client/components/notes.vue
index 2bf6327b09..dc93c1f6c4 100644
--- a/src/client/components/notes.vue
+++ b/src/client/components/notes.vue
@@ -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._prInjectionId_ || 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);">
diff --git a/src/client/pages/my-settings/index.vue b/src/client/pages/my-settings/index.vue
index 65300fac83..4742793f2b 100644
--- a/src/client/pages/my-settings/index.vue
+++ b/src/client/pages/my-settings/index.vue
@@ -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');
},
diff --git a/src/models/entities/user-profile.ts b/src/models/entities/user-profile.ts
index 278e8ce815..a89d7364f3 100644
--- a/src/models/entities/user-profile.ts
+++ b/src/models/entities/user-profile.ts
@@ -125,6 +125,11 @@ export class UserProfile {
})
public carefulBot: boolean;
+ @Column('boolean', {
+ default: true,
+ })
+ public injectFeaturedNote: boolean;
+
@Column({
...id(),
nullable: true
diff --git a/src/models/repositories/note.ts b/src/models/repositories/note.ts
index 5d0a8768d1..59ec63b45c 100644
--- a/src/models/repositories/note.ts
+++ b/src/models/repositories/note.ts
@@ -196,7 +196,8 @@ export class NoteRepository extends Repository<Note> {
renoteId: note.renoteId,
mentions: note.mentions.length > 0 ? note.mentions : undefined,
uri: note.uri || undefined,
- _prInjectionId_: (note as any)._prInjectionId_ || undefined,
+ _featuredId_: (note as any)._featuredId_ || undefined,
+ _prId_: (note as any)._prId_ || undefined,
...(opts.detail ? {
reply: note.replyId ? this.pack(note.replyId, meId, {
diff --git a/src/models/repositories/user.ts b/src/models/repositories/user.ts
index 4d96fbe95c..1d669feb5e 100644
--- a/src/models/repositories/user.ts
+++ b/src/models/repositories/user.ts
@@ -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,
diff --git a/src/server/api/common/inject-featured.ts b/src/server/api/common/inject-featured.ts
new file mode 100644
index 0000000000..ce6d421bb1
--- /dev/null
+++ b/src/server/api/common/inject-featured.ts
@@ -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);
+}
diff --git a/src/server/api/common/inject-promo.ts b/src/server/api/common/inject-promo.ts
index 60817329c3..f694ce6ea0 100644
--- a/src/server/api/common/inject-promo.ts
+++ b/src/server/api/common/inject-promo.ts
@@ -4,14 +4,14 @@ import { User } from '../../../models/entities/user';
import { PromoReads, PromoNotes, Notes, Users } from '../../../models';
import { ensure } from '../../../prelude/ensure';
-export async function injectPromo(user: User, timeline: Note[]) {
+export async function injectPromo(timeline: Note[], user?: User | null) {
if (timeline.length < 5) return;
// TODO: readやexpireフィルタはクエリ側でやる
- const reads = await PromoReads.find({
+ const reads = user ? await PromoReads.find({
userId: user.id
- });
+ }) : [];
let promos = await PromoNotes.find();
@@ -20,15 +20,15 @@ export async function injectPromo(user: User, timeline: Note[]) {
if (promos.length === 0) return;
+ // Pick random promo
const promo = promos[Math.floor(Math.random() * promos.length)];
- // Pick random promo
const note = await Notes.findOne(promo.noteId).then(ensure);
// Join
note.user = await Users.findOne(note.userId).then(ensure);
- (note as any)._prInjectionId_ = rndstr('a-z0-9', 8);
+ (note as any)._prId_ = rndstr('a-z0-9', 8);
// Inject promo
timeline.splice(3, 0, note);
diff --git a/src/server/api/common/signup.ts b/src/server/api/common/signup.ts
index f0eb27e5e4..b6e13b36f1 100644
--- a/src/server/api/common/signup.ts
+++ b/src/server/api/common/signup.ts
@@ -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,
}));
diff --git a/src/server/api/define.ts b/src/server/api/define.ts
index d4ca5aa809..1fd4543bd0 100644
--- a/src/server/api/define.ts
+++ b/src/server/api/define.ts
@@ -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']>
diff --git a/src/server/api/endpoints/i/update.ts b/src/server/api/endpoints/i/update.ts
index 08c37a4f63..5c4a9576e1 100644
--- a/src/server/api/endpoints/i/update.ts
+++ b/src/server/api/endpoints/i/update.ts
@@ -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) {
diff --git a/src/server/api/endpoints/notes/featured.ts b/src/server/api/endpoints/notes/featured.ts
index 5fc60eeccf..0dc705de7a 100644
--- a/src/server/api/endpoints/notes/featured.ts
+++ b/src/server/api/endpoints/notes/featured.ts
@@ -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');
diff --git a/src/server/api/endpoints/notes/global-timeline.ts b/src/server/api/endpoints/notes/global-timeline.ts
index 0f69896de2..26b0cb0f5a 100644
--- a/src/server/api/endpoints/notes/global-timeline.ts
+++ b/src/server/api/endpoints/notes/global-timeline.ts
@@ -9,6 +9,7 @@ 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: {
@@ -90,7 +91,8 @@ export default define(meta, async (ps, user) => {
const timeline = await query.take(ps.limit!).getMany();
- await injectPromo(user, timeline);
+ await injectPromo(timeline, user);
+ await injectFeatured(timeline, user);
process.nextTick(() => {
if (user) {
diff --git a/src/server/api/endpoints/notes/hybrid-timeline.ts b/src/server/api/endpoints/notes/hybrid-timeline.ts
index f30fbab8a1..b0a73d1d7d 100644
--- a/src/server/api/endpoints/notes/hybrid-timeline.ts
+++ b/src/server/api/endpoints/notes/hybrid-timeline.ts
@@ -11,6 +11,7 @@ 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: {
@@ -170,7 +171,8 @@ export default define(meta, async (ps, user) => {
const timeline = await query.take(ps.limit!).getMany();
- await injectPromo(user, timeline);
+ await injectPromo(timeline, user);
+ await injectFeatured(timeline, user);
process.nextTick(() => {
if (user) {
diff --git a/src/server/api/endpoints/notes/local-timeline.ts b/src/server/api/endpoints/notes/local-timeline.ts
index 68558fb84b..a74dc3b15c 100644
--- a/src/server/api/endpoints/notes/local-timeline.ts
+++ b/src/server/api/endpoints/notes/local-timeline.ts
@@ -11,6 +11,7 @@ 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: {
@@ -123,7 +124,8 @@ export default define(meta, async (ps, user) => {
const timeline = await query.take(ps.limit!).getMany();
- await injectPromo(user, timeline);
+ await injectPromo(timeline, user);
+ await injectFeatured(timeline, user);
process.nextTick(() => {
if (user) {
diff --git a/src/server/api/endpoints/notes/timeline.ts b/src/server/api/endpoints/notes/timeline.ts
index 8edf303e0d..3eed9f0ca8 100644
--- a/src/server/api/endpoints/notes/timeline.ts
+++ b/src/server/api/endpoints/notes/timeline.ts
@@ -9,6 +9,7 @@ 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: {
@@ -156,7 +157,8 @@ export default define(meta, async (ps, user) => {
const timeline = await query.take(ps.limit!).getMany();
- await injectPromo(user, timeline);
+ await injectPromo(timeline, user);
+ await injectFeatured(timeline, user);
process.nextTick(() => {
if (user) {