diff options
| author | syuilo <Syuilotan@yahoo.co.jp> | 2020-02-18 19:05:11 +0900 |
|---|---|---|
| committer | syuilo <Syuilotan@yahoo.co.jp> | 2020-02-18 19:05:11 +0900 |
| commit | d9986b7a2fabffff50068f4114a16d315941591f (patch) | |
| tree | 7627c97a74a4e175cf8e0357cd380d21ac15b898 /src | |
| parent | Improve paging (diff) | |
| download | misskey-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.vue | 12 | ||||
| -rw-r--r-- | src/client/components/note.vue | 7 | ||||
| -rw-r--r-- | src/client/components/notes.vue | 2 | ||||
| -rw-r--r-- | src/client/pages/my-settings/index.vue | 9 | ||||
| -rw-r--r-- | src/models/entities/user-profile.ts | 5 | ||||
| -rw-r--r-- | src/models/repositories/note.ts | 3 | ||||
| -rw-r--r-- | src/models/repositories/user.ts | 1 | ||||
| -rw-r--r-- | src/server/api/common/inject-featured.ts | 45 | ||||
| -rw-r--r-- | src/server/api/common/inject-promo.ts | 10 | ||||
| -rw-r--r-- | src/server/api/common/signup.ts | 1 | ||||
| -rw-r--r-- | src/server/api/define.ts | 1 | ||||
| -rw-r--r-- | src/server/api/endpoints/i/update.ts | 5 | ||||
| -rw-r--r-- | src/server/api/endpoints/notes/featured.ts | 1 | ||||
| -rw-r--r-- | src/server/api/endpoints/notes/global-timeline.ts | 4 | ||||
| -rw-r--r-- | src/server/api/endpoints/notes/hybrid-timeline.ts | 4 | ||||
| -rw-r--r-- | src/server/api/endpoints/notes/local-timeline.ts | 4 | ||||
| -rw-r--r-- | src/server/api/endpoints/notes/timeline.ts | 4 |
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) { |