1
0
mirror of https://github.com/misskey-dev/misskey.git synced 2026-05-11 21:15:27 +02:00

Compare commits

..

33 Commits

Author SHA1 Message Date
syuilo
89105f5641 2.38.2 2018-06-13 05:25:27 +09:00
syuilo
1813d17b4c Fix bug 2018-06-13 05:24:44 +09:00
syuilo
ce27b36fd0 Fix bug 2018-06-13 05:21:55 +09:00
syuilo
e635a87628 2.38.1 2018-06-13 05:16:53 +09:00
syuilo
80c52433cc Fix bug 2018-06-13 05:15:26 +09:00
syuilo
1472f0b141 Fix #1712 2018-06-13 05:11:55 +09:00
syuilo
4d914f5c0a 2.38.0 2018-06-12 19:07:36 +09:00
syuilo
0318f7344f Fix bug 2018-06-12 19:05:40 +09:00
syuilo
413fbb3d0c モバイルでもハッシュタグを検索できるように 2018-06-12 19:03:57 +09:00
syuilo
8bc47baf4f #1710 2018-06-12 18:54:36 +09:00
syuilo
e3f6d42a47 Fix bug 2018-06-12 11:38:44 +09:00
syuilo
8230935fd3 🎨 2018-06-12 11:27:35 +09:00
syuilo
f968d05ea0 2.37.7 2018-06-12 09:10:52 +09:00
syuilo
d6e5dc2167 Fix bugs 2018-06-12 09:10:34 +09:00
syuilo
460147fea2 2.37.6 2018-06-12 08:59:36 +09:00
syuilo
cea44834bb Improve usability 2018-06-12 08:58:50 +09:00
syuilo
1af50fd7b8 冗長なハッシュタグの表示を無くした 2018-06-12 08:43:48 +09:00
syuilo
b18013025f 🎨 2018-06-12 08:09:27 +09:00
syuilo
399eb60809 2.37.5 2018-06-12 02:47:17 +09:00
syuilo
ed67e3506b ✌️ 2018-06-12 02:46:54 +09:00
syuilo
d8ff37fc45 ✌️ 2018-06-12 02:28:28 +09:00
syuilo
2fcc3bb1ea 2.37.4 2018-06-12 02:19:00 +09:00
syuilo
2e680c3d1e Fix bug 2018-06-12 02:18:29 +09:00
syuilo
af0a0ef41b Merge branch 'master' of https://github.com/syuilo/misskey 2018-06-12 02:03:26 +09:00
syuilo
bbfccb0bbf 🎨 2018-06-12 02:03:18 +09:00
syuilo
c89eb5d69f Merge pull request #1705 from syuilo/l10n_master
New Crowdin translations
2018-06-12 02:03:06 +09:00
syuilo
ebde84214e Improve hashtag trend detection 2018-06-12 02:00:05 +09:00
syuilo
03fbae7b6d 変数調整 2018-06-12 01:51:51 +09:00
syuilo
f90e9596d4 Fix bug 2018-06-12 01:48:29 +09:00
syuilo
944f9524e2 Fix bug 2018-06-12 01:45:58 +09:00
syuilo
c61050244e 変数調整 2018-06-12 01:43:56 +09:00
syuilo
90337adbbc Improve hashtag trend detection 2018-06-12 01:41:17 +09:00
syuilo
7b67e41c5b New translations ja.yml (Polish) 2018-06-11 19:22:16 +09:00
21 changed files with 262 additions and 31 deletions

View File

@@ -258,6 +258,7 @@ common/views/widgets/posts-monitor.vue:
common/views/widgets/hashtags.vue:
title: "ハッシュタグ"
count: "{}人が投稿"
empty: "トレンドなし"
common/views/widgets/server.vue:
title: "サーバー情報"

View File

@@ -70,7 +70,7 @@ common:
donation: "Dotacje"
nav: "Nawigacja"
tips: "Wskazówki"
hashtags: "ハッシュタグ"
hashtags: "Hashtagi"
deck:
widgets: "Widżety"
home: "Strona główna"
@@ -226,8 +226,8 @@ common/views/widgets/posts-monitor.vue:
title: "Wykres wpisów"
toggle: "Przełącz widok"
common/views/widgets/hashtags.vue:
title: "ハッシュタグ"
count: "{}人が投稿"
title: "Hashtagi"
count: "Wspomniany przez {} użytkowników"
common/views/widgets/server.vue:
title: "Informacje o serwerze"
toggle: "Przełącz widok"

View File

@@ -1,8 +1,8 @@
{
"name": "misskey",
"author": "syuilo <i@syuilo.com>",
"version": "2.37.3",
"clientVersion": "1.0.6453",
"version": "2.38.2",
"clientVersion": "1.0.6486",
"codename": "nighthike",
"main": "./built/index.js",
"private": true,

View File

@@ -40,6 +40,17 @@ export default Vue.component('mk-note-html', {
ast = this.ast;
}
if (ast.filter(x => x.type != 'hashtag').length == 0) {
return;
}
while (ast[ast.length - 1] && (
ast[ast.length - 1].type == 'hashtag' ||
(ast[ast.length - 1].type == 'text' && ast[ast.length - 1].content == ' ') ||
(ast[ast.length - 1].type == 'text' && ast[ast.length - 1].content == '\n'))) {
ast.pop();
}
// Parse ast to DOM
const els = flatten(ast.map(token => {
switch (token.type) {
@@ -92,7 +103,7 @@ export default Vue.component('mk-note-html', {
case 'hashtag':
return createElement('a', {
attrs: {
href: `${url}/search?q=${token.content}`,
href: `${url}/tags/${token.hashtag}`,
target: '_blank'
}
}, token.content);

View File

@@ -49,7 +49,8 @@ export default Vue.extend({
polylinePoints: '',
polygonPoints: '',
headX: null,
headY: null
headY: null,
clock: null
};
},
watch: {
@@ -59,6 +60,12 @@ export default Vue.extend({
},
created() {
this.draw();
// Vueが何故かWatchを発動させない場合があるので
this.clock = setInterval(this.draw, 1000);
},
beforeDestroy() {
clearInterval(this.clock);
},
methods: {
draw() {

View File

@@ -5,7 +5,8 @@
<div class="mkw-hashtags--body" :data-mobile="platform == 'mobile'">
<p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
<div v-else>
<p class="empty" v-else-if="stats.length == 0">%fa:exclamation-circle%%i18n:@empty%</p>
<transition-group v-else tag="div" name="chart">
<div v-for="stat in stats" :key="stat.tag">
<div class="tag">
<router-link :to="`/tags/${ stat.tag }`" :title="stat.tag">#{{ stat.tag }}</router-link>
@@ -13,7 +14,7 @@
</div>
<x-chart class="chart" :src="stat.chart"/>
</div>
</div>
</transition-group>
</div>
</mk-widget-container>
</div>
@@ -65,8 +66,9 @@ export default define({
root(isDark)
.mkw-hashtags--body
> .fetching
> .empty
margin 0
padding 12px 16px
padding 16px
text-align center
color #aaa
@@ -74,10 +76,13 @@ root(isDark)
margin-right 4px
> div
.chart-move
transition transform 1s ease
> div
display flex
align-items center
padding 16px
padding 14px 16px
&:not(:last-child)
border-bottom solid 1px isDark ? #393f4f : #eee

View File

@@ -50,6 +50,7 @@ import * as XDraggable from 'vuedraggable';
import getKao from '../../../common/scripts/get-kao';
import MkVisibilityChooser from '../../../common/views/components/visibility-chooser.vue';
import parse from '../../../../../text/parse';
import { host } from '../../../config';
export default Vue.extend({
components: {
@@ -129,6 +130,7 @@ export default Vue.extend({
// 自分は除外
if (this.$store.state.i.username == x.username && x.host == null) return;
if (this.$store.state.i.username == x.username && x.host == host) return;
// 重複は除外
if (this.text.indexOf(`${mention} `) != -1) return;

View File

@@ -5,7 +5,7 @@
<div class="gqpwvtwtprsbmnssnbicggtwqhmylhnq">
<template v-if="edit">
<header>
<select v-model="widgetAdderSelected">
<select v-model="widgetAdderSelected" @change="addWidget">
<option value="profile">%i18n:common.widgets.profile%</option>
<option value="analog-clock">%i18n:common.widgets.analog-clock%</option>
<option value="calendar">%i18n:common.widgets.calendar%</option>
@@ -30,20 +30,15 @@
<option value="nav">%i18n:common.widgets.nav%</option>
<option value="tips">%i18n:common.widgets.tips%</option>
</select>
<button @click="addWidget">%i18n:@add%</button>
</header>
<x-draggable
:list="column.widgets"
:options="{ handle: '.handle', animation: 150 }"
:options="{ animation: 150 }"
@sort="onWidgetSort"
>
<div v-for="widget in column.widgets" class="customize-container" :key="widget.id">
<header>
<span class="handle">%fa:bars%</span>{{ widget.name }}<button class="remove" @click="removeWidget(widget)">%fa:times%</button>
</header>
<div @click="widgetFunc(widget.id)">
<component :is="`mkw-${widget.name}`" :widget="widget" :ref="widget.id" :is-customize-mode="true" platform="deck"/>
</div>
<div v-for="widget in column.widgets" class="customize-container" :key="widget.id" @contextmenu.stop.prevent="widgetFunc(widget.id)">
<button class="remove" @click="removeWidget(widget)">%fa:times%</button>
<component :is="`mkw-${widget.name}`" :widget="widget" :ref="widget.id" :is-customize-mode="true" platform="deck"/>
</div>
</x-draggable>
</template>
@@ -142,6 +137,13 @@ export default Vue.extend({
root(isDark)
.gqpwvtwtprsbmnssnbicggtwqhmylhnq
> header
padding 16px
> *
width 100%
padding 4px
.widget, .customize-container
margin 8px
@@ -149,7 +151,21 @@ root(isDark)
margin-top 0
.customize-container
background #fff
cursor move
> *:not(.remove)
pointer-events none
> .remove
position absolute
z-index 1
top 8px
right 8px
width 32px
height 32px
color #fff
background rgba(#000, 0.7)
border-radius 4px
> header
color isDark ? #fff : #000

View File

@@ -42,6 +42,7 @@ import MkUserLists from './views/pages/user-lists.vue';
import MkUserList from './views/pages/user-list.vue';
import MkSettings from './views/pages/settings.vue';
import MkOthello from './views/pages/othello.vue';
import MkTag from './views/pages/tag.vue';
Vue.use(MdCard);
Vue.use(MdButton);
@@ -88,6 +89,7 @@ init((launch) => {
{ path: '/i/drive/file/:file', component: MkDrive },
{ path: '/selectdrive', component: MkSelectDrive },
{ path: '/search', component: MkSearch },
{ path: '/tags/:tag', component: MkTag },
{ path: '/othello', name: 'othello', component: MkOthello },
{ path: '/othello/:game', component: MkOthello },
{ path: '/@:user', component: MkUser },

View File

@@ -46,6 +46,7 @@ import * as XDraggable from 'vuedraggable';
import MkVisibilityChooser from '../../../common/views/components/visibility-chooser.vue';
import getKao from '../../../common/scripts/get-kao';
import parse from '../../../../../text/parse';
import { host } from '../../../config';
export default Vue.extend({
components: {
@@ -123,6 +124,7 @@ export default Vue.extend({
// 自分は除外
if (this.$store.state.i.username == x.username && x.host == null) return;
if (this.$store.state.i.username == x.username && x.host == host) return;
// 重複は除外
if (this.text.indexOf(`${mention} `) != -1) return;

View File

@@ -0,0 +1,81 @@
<template>
<mk-ui>
<span slot="header">%fa:hashtag%{{ $route.params.tag }}</span>
<main>
<p v-if="!fetching && empty">%fa:search%{{ q }}に関する投稿は見つかりませんでした</p>
<mk-notes ref="timeline" :more="existMore ? more : null"/>
</main>
</mk-ui>
</template>
<script lang="ts">
import Vue from 'vue';
import Progress from '../../../common/scripts/loading';
const limit = 20;
export default Vue.extend({
data() {
return {
fetching: true,
moreFetching: false,
existMore: false,
offset: 0,
empty: false
};
},
watch: {
$route: 'fetch'
},
mounted() {
this.$nextTick(() => {
this.fetch();
});
},
methods: {
fetch() {
this.fetching = true;
Progress.start();
(this.$refs.timeline as any).init(() => new Promise((res, rej) => {
(this as any).api('notes/search_by_tag', {
limit: limit + 1,
offset: this.offset,
tag: this.$route.params.tag
}).then(notes => {
if (notes.length == 0) this.empty = true;
if (notes.length == limit + 1) {
notes.pop();
this.existMore = true;
}
res(notes);
this.fetching = false;
Progress.done();
}, rej);
}));
},
more() {
this.offset += limit;
const promise = (this as any).api('notes/search_by_tag', {
limit: limit + 1,
offset: this.offset,
tag: this.$route.params.tag
});
promise.then(notes => {
if (notes.length == limit + 1) {
notes.pop();
} else {
this.existMore = false;
}
notes.forEach(n => (this.$refs.timeline as any).append(n));
this.moreFetching = false;
});
return promise;
}
}
});
</script>

View File

@@ -48,6 +48,11 @@ export type INote = {
repliesCount: number;
reactionCounts: any;
mentions: mongo.ObjectID[];
mentionedRemoteUsers: Array<{
uri: string;
username: string;
host: string;
}>;
/**
* public ... 公開
@@ -289,7 +294,7 @@ export const pack = async (
// Poll
if (meId && _note.poll && !hide) {
_note.poll = (async (poll) => {
_note.poll = (async poll => {
const vote = await PollVote
.findOne({
userId: meId,

View File

@@ -15,6 +15,11 @@ const log = debug('misskey:activitypub');
export default async function(resolver: Resolver, actor: IRemoteUser, activity: IAnnounce, note: INote): Promise<void> {
const uri = activity.id || activity;
// アナウンサーが凍結されていたらスキップ
if (actor.isSuspended) {
return;
}
if (typeof uri !== 'string') {
throw new Error('invalid announce');
}

View File

@@ -1,6 +1,6 @@
import config from '../../../config';
export default tag => ({
export default (tag: string) => ({
type: 'Hashtag',
href: `${config.url}/tags/${encodeURIComponent(tag)}`,
name: '#' + tag

View File

@@ -0,0 +1,9 @@
export default (mention: {
uri: string;
username: string;
host: string;
}) => ({
type: 'Mention',
href: mention.uri,
name: `@${mention.username}@${mention.host}`
});

View File

@@ -1,5 +1,6 @@
import renderDocument from './document';
import renderHashtag from './hashtag';
import renderMention from './mention';
import config from '../../../config';
import DriveFile from '../../../models/drive-file';
import Note, { INote } from '../../../models/note';
@@ -45,6 +46,18 @@ export default async function renderNote(note: INote, dive = true) {
const attributedTo = `${config.url}/users/${user._id}`;
const mentions = note.mentionedRemoteUsers && note.mentionedRemoteUsers.length > 0
? note.mentionedRemoteUsers.map(x => x.uri)
: [];
const cc = ['public', 'home', 'followers'].includes(note.visibility)
? [`${attributedTo}/followers`].concat(mentions)
: [];
const hashtagTags = (note.tags || []).map(renderHashtag);
const mentionTags = (note.mentionedRemoteUsers || []).map(renderMention);
const tag = hashtagTags.concat(mentionTags)
return {
id: `${config.url}/notes/${note._id}`,
type: 'Note',
@@ -52,9 +65,9 @@ export default async function renderNote(note: INote, dive = true) {
content: toHtml(note),
published: note.createdAt.toISOString(),
to: 'https://www.w3.org/ns/activitystreams#Public',
cc: `${attributedTo}/followers`,
cc,
inReplyTo,
attachment: (await promisedFiles).map(renderDocument),
tag: (note.tags || []).map(renderHashtag)
tag
};
}

View File

@@ -1,13 +1,26 @@
import Note from '../../../../models/note';
/*
トレンドに載るためには「『直近a分間のユニーク投稿数が今からa分前今からb分前の間のユニーク投稿数のn倍以上』のハッシュタグの上位5位以内に入る」ことが必要
ユニーク投稿数とはそのハッシュタグと投稿ユーザーのペアのカウントで、例えば同じユーザーが複数回同じハッシュタグを投稿してもそのハッシュタグのユニーク投稿数は1とカウントされる
*/
const rangeA = 1000 * 60 * 30; // 30分
const rangeB = 1000 * 60 * 120; // 2時間
const coefficient = 1.5; // 「n倍」の部分
const requiredUsers = 3; // 最低何人がそのタグを投稿している必要があるか
const max = 5;
/**
* Get trends of hashtags
*/
module.exports = () => new Promise(async (res, rej) => {
//#region 1. 直近Aの内に投稿されたハッシュタグ(とユーザーのペア)を集計
const data = await Note.aggregate([{
$match: {
createdAt: {
$gt: new Date(Date.now() - 1000 * 60 * 60)
$gt: new Date(Date.now() - rangeA)
},
tags: {
$exists: true,
@@ -26,6 +39,7 @@ module.exports = () => new Promise(async (res, rej) => {
userId: any;
}
}>;
//#endregion
if (data.length == 0) {
return res([]);
@@ -33,6 +47,7 @@ module.exports = () => new Promise(async (res, rej) => {
const tags = [];
// カウント
data.map(x => x._id).forEach(x => {
const i = tags.findIndex(tag => tag.name == x.tags);
if (i != -1) {
@@ -45,11 +60,45 @@ module.exports = () => new Promise(async (res, rej) => {
}
});
const hots = tags
// 最低要求投稿者数を下回るならカットする
const limitedTags = tags.filter(tag => tag.count >= requiredUsers);
//#region 2. 1で取得したそれぞれのタグについて、「直近a分間のユニーク投稿数が今からa分前今からb分前の間のユニーク投稿数のn倍以上」かどうかを判定する
const hotsPromises = limitedTags.map(async tag => {
const passedCount = (await Note.distinct('userId', {
tags: tag.name,
createdAt: {
$lt: new Date(Date.now() - rangeA),
$gt: new Date(Date.now() - rangeB)
}
}) as any).length;
if (tag.count >= (passedCount * coefficient)) {
return tag;
} else {
return null;
}
});
//#endregion
// タグを人気順に並べ替え
let hots = (await Promise.all(hotsPromises))
.filter(x => x != null)
.sort((a, b) => b.count - a.count)
.map(tag => tag.name)
.slice(0, 5);
.slice(0, max);
//#region 3. もし上記の方法でのトレンド抽出の結果、求められているタグ数に達しなければ「ただ単に現在投稿数が多いハッシュタグ」に切り替える
if (hots.length < max) {
hots = hots.concat(tags
.filter(tag => hots.indexOf(tag.name) == -1)
.sort((a, b) => b.count - a.count)
.map(tag => tag.name)
.slice(0, max - hots.length));
}
//#endregion
//#region 2(または3)で話題と判定されたタグそれぞれについて過去の投稿数グラフを取得する
const countPromises: Array<Promise<any[]>> = [];
const range = 20;
@@ -75,6 +124,7 @@ module.exports = () => new Promise(async (res, rej) => {
$gt: new Date(Date.now() - (interval * range))
}
})));
//#endregion
const stats = hots.map((tag, i) => ({
tag,

View File

@@ -4,7 +4,7 @@ import * as debug from 'debug';
import User, { IUser } from '../../../models/user';
import Mute from '../../../models/mute';
import { pack as packNote } from '../../../models/note';
import { pack as packNote, pack } from '../../../models/note';
import readNotification from '../common/read-notification';
import call from '../call';
import { IApp } from '../../../models/app';
@@ -48,6 +48,14 @@ export default async function(
}
//#endregion
// Renoteなら再pack
if (x.type == 'note' && x.body.renoteId != null) {
x.body.renote = await pack(x.body.renoteId, user, {
detail: true
});
data = JSON.stringify(x);
}
connection.send(data);
} catch (e) {
connection.send(data);

View File

@@ -3,6 +3,7 @@ import * as redis from 'redis';
import { IUser } from '../../../models/user';
import Mute from '../../../models/mute';
import { pack } from '../../../models/note';
export default async function(
request: websocket.request,
@@ -31,6 +32,13 @@ export default async function(
}
//#endregion
// Renoteなら再pack
if (note.renoteId != null) {
note.renote = await pack(note.renoteId, user, {
detail: true
});
}
connection.send(JSON.stringify({
type: 'note',
body: note

View File

@@ -329,7 +329,12 @@ export default async (user: IUser, data: {
if (mentionedUsers.length > 0) {
Note.update({ _id: note._id }, {
$set: {
mentions: mentionedUsers.map(u => u._id)
mentions: mentionedUsers.map(u => u._id),
mentionedRemoteUsers: mentionedUsers.filter(u => isRemoteUser(u)).map(u => ({
uri: (u as IRemoteUser).uri,
username: u.username,
host: u.host
}))
}
});
}

View File

@@ -20,6 +20,7 @@ export default async function(user: IUser, note: INote) {
$set: {
deletedAt: new Date(),
text: null,
tags: [],
mediaIds: [],
poll: null
}