Merge pull request #2428 from h3poteto/iss-2258

refs #2258 Apply filter for each timelines
This commit is contained in:
AkiraFukushima 2021-05-22 14:55:51 +09:00 committed by GitHub
commit 6b9b38e643
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 308 additions and 92 deletions

View File

@ -84,7 +84,8 @@ const state = (): TimelineSpaceState => {
local: true,
public: true
},
sns: 'mastodon'
sns: 'mastodon',
filters: []
}
}

View File

@ -16,7 +16,8 @@ describe('TimelineSpace', () => {
local: unreadSettings.Local.default,
public: unreadSettings.Public.default
},
sns: 'mastodon'
sns: 'mastodon',
filters: []
}
})

View File

@ -0,0 +1,106 @@
import filtered from '@/utils/filter'
import { Entity } from 'megalodon'
describe('filter', () => {
describe('whole word is enabled', () => {
describe('Only asci', () => {
const filters = [
{
id: '1',
phrase: 'Fedi',
context: ['home'],
expires_at: null,
irreversible: false,
whole_word: true
} as Entity.Filter
]
it('should not be matched', () => {
const status =
'Pleroma is social networking software compatible with other Fediverse software such as Mastodon, Misskey, Pixelfed and many others.'
const res = filtered(status, filters)
expect(res).toBeFalsy()
})
it('should be matched', () => {
const status =
'Pleroma is social networking software compatible with other Fedi software such as Mastodon, Misskey, Pixelfed and many others.'
const res = filtered(status, filters)
expect(res).toBeTruthy()
})
})
describe('With Japanese', () => {
const filters = [
{
id: '1',
phrase: 'ミニブログ',
context: ['home'],
expires_at: null,
irreversible: false,
whole_word: true
} as Entity.Filter
]
it('should be matched', () => {
const status =
'マストドン (Mastodon) はミニブログサービスを提供するためのフリーソフトウェア、またはこれが提供する連合型のソーシャルネットワークサービスである'
const res = filtered(status, filters)
expect(res).toBeTruthy()
})
it('should not be matched', () => {
const status =
'「脱中央集権型」 (decentralized) のマストドンのサーバーはだれでも自由に運用する事が可能であり、利用者は通常このサーバーの一つを選んで所属するが、異なるサーバーに属する利用者間のコミュニケーションも容易である'
const res = filtered(status, filters)
expect(res).toBeFalsy()
})
})
})
describe('whole word is disabled', () => {
describe('Only asci', () => {
const filters = [
{
id: '1',
phrase: 'Fedi',
context: ['home'],
expires_at: null,
irreversible: false,
whole_word: false
} as Entity.Filter
]
it('should be matched', () => {
const status =
'Pleroma is social networking software compatible with other Fediverse software such as Mastodon, Misskey, Pixelfed and many others.'
const res = filtered(status, filters)
expect(res).toBeTruthy()
})
it('should be matched', () => {
const status =
'Pleroma is social networking software compatible with other Fedi software such as Mastodon, Misskey, Pixelfed and many others.'
const res = filtered(status, filters)
expect(res).toBeTruthy()
})
})
describe('With Japanese', () => {
const filters = [
{
id: '1',
phrase: 'ミニブログ',
context: ['home'],
expires_at: null,
irreversible: false,
whole_word: true
} as Entity.Filter
]
it('should be matched', () => {
const status =
'マストドン (Mastodon) はミニブログサービスを提供するためのフリーソフトウェア、またはこれが提供する連合型のソーシャルネットワークサービスである'
const res = filtered(status, filters)
expect(res).toBeTruthy()
})
it('should not be matched', () => {
const status =
'「脱中央集権型」 (decentralized) のマストドンのサーバーはだれでも自由に運用する事が可能であり、利用者は通常このサーバーの一つを選んで所属するが、異なるサーバーに属する利用者間のコミュニケーションも容易である'
const res = filtered(status, filters)
expect(res).toBeFalsy()
})
})
})
})

View File

@ -9,6 +9,7 @@
:message="item"
:focused="item.uri + item.id === focusedId"
:overlaid="modalOpened"
:filters="filters"
v-on:update="updateToot"
v-on:delete="deleteToot"
@focusNext="focusNext"
@ -44,18 +45,21 @@ export default {
}
},
computed: {
...mapState('TimelineSpace/Contents/Home', {
timeline: state => state.timeline,
lazyLoading: state => state.lazyLoading,
heading: state => state.heading,
unread: state => state.unreadTimeline,
showReblogs: state => state.showReblogs,
showReplies: state => state.showReplies
}),
...mapState({
openSideBar: state => state.TimelineSpace.Contents.SideBar.openSideBar,
backgroundColor: state => state.App.theme.background_color,
startReload: state => state.TimelineSpace.HeaderMenu.reload,
timeline: state => state.TimelineSpace.Contents.Home.timeline,
lazyLoading: state => state.TimelineSpace.Contents.Home.lazyLoading,
heading: state => state.TimelineSpace.Contents.Home.heading,
unread: state => state.TimelineSpace.Contents.Home.unreadTimeline,
showReblogs: state => state.TimelineSpace.Contents.Home.showReblogs,
showReplies: state => state.TimelineSpace.Contents.Home.showReplies
openSideBar: state => state.TimelineSpace.Contents.SideBar.openSideBar,
startReload: state => state.TimelineSpace.HeaderMenu.reload
}),
...mapGetters('TimelineSpace/Modals', ['modalOpened']),
...mapGetters('TimelineSpace/Contents/Home', ['filters']),
shortcutEnabled: function () {
if (this.modalOpened) {
return false

View File

@ -9,6 +9,7 @@
:message="item"
:focused="item.id === focusedId"
:overlaid="modalOpened"
:filters="filters"
v-on:update="updateToot"
@focusNext="focusNext"
@focusPrev="focusPrev"
@ -51,7 +52,7 @@ export default {
heading: state => state.TimelineSpace.Contents.Notifications.heading,
unread: state => state.TimelineSpace.Contents.Notifications.unreadNotifications
}),
...mapGetters('TimelineSpace/Contents/Notifications', ['handledNotifications']),
...mapGetters('TimelineSpace/Contents/Notifications', ['handledNotifications', 'filters']),
...mapGetters('TimelineSpace/Modals', ['modalOpened']),
shortcutEnabled: function () {
if (this.modalOpened) {

View File

@ -9,6 +9,7 @@
:message="item"
:focused="item.uri + item.id === focusedId"
:overlaid="modalOpened"
:filters="filters"
v-on:update="updateToot"
v-on:delete="deleteToot"
@focusNext="focusNext"
@ -56,6 +57,7 @@ export default {
startReload: state => state.TimelineSpace.HeaderMenu.reload,
unreadNotification: state => state.TimelineSpace.unreadNotification
}),
...mapGetters('TimelineSpace/Contents/Public', ['filters']),
...mapGetters('TimelineSpace/Modals', ['modalOpened']),
shortcutEnabled: function () {
if (this.modalOpened) {

View File

@ -1,14 +1,17 @@
<template>
<div class="tabs" id="sidebar_tabs">
<el-tabs v-model="activeName" @tab-click="handleClick" stretch>
<el-tab-pane label="Posts" name="posts"><Posts :account="account" :buffer="buffer" /></el-tab-pane>
<el-tab-pane label="Posts and replies" name="posts_and_replies"><PostsAndReplies :account="account" :buffer="buffer" /></el-tab-pane>
<el-tab-pane label="Media" name="media"><Media :account="account" :buffer="buffer" /></el-tab-pane>
<el-tab-pane label="Posts" name="posts"><Posts :account="account" :buffer="buffer" :filters="filters" /></el-tab-pane>
<el-tab-pane label="Posts and replies" name="posts_and_replies"
><PostsAndReplies :account="account" :buffer="buffer" :filters="filters"
/></el-tab-pane>
<el-tab-pane label="Media" name="media"><Media :account="account" :buffer="buffer" :filters="filters" /></el-tab-pane>
</el-tabs>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
import Posts from './Timeline/Posts'
import PostsAndReplies from './Timeline/PostsAndReplies'
import Media from './Timeline/Media'
@ -28,6 +31,9 @@ export default {
buffer: 200
}
},
computed: {
...mapGetters('TimelineSpace/Contents/SideBar/AccountProfile/Timeline', ['filters'])
},
mounted() {
const timeline = document.getElementById('sidebar_tabs')
if (timeline !== undefined && timeline !== null) {

View File

@ -8,6 +8,7 @@
:key="item.id"
:focused="item.uri + item.id === focusedId"
:overlaid="modalOpened"
:filters="filters"
v-on:update="updateToot"
v-on:delete="deleteToot"
@focusNext="focusNext"
@ -30,7 +31,7 @@ import { Event } from '~/src/renderer/components/event'
export default {
name: 'media',
props: ['account', 'buffer'],
props: ['account', 'buffer', 'filters'],
components: { Toot },
data() {
return {

View File

@ -7,6 +7,7 @@
:focused="message.uri + message.id === focusedId"
:pinned="true"
:overlaid="modalOpened"
:filters="filters"
v-on:update="updatePinnedToot"
v-on:delete="deleteToot"
@focusNext="focusNext"
@ -24,6 +25,7 @@
:key="item.id"
:focused="item.uri + item.id === focusedId"
:overlaid="modalOpened"
:filters="filters"
v-on:update="updateToot"
v-on:delete="deleteToot"
@focusNext="focusNext"
@ -46,7 +48,7 @@ import { Event } from '~/src/renderer/components/event'
export default {
name: 'posts',
props: ['account', 'buffer'],
props: ['account', 'buffer', 'filters'],
components: { Toot },
data() {
return {

View File

@ -8,6 +8,7 @@
:key="item.id"
:focused="item.uri + item.id === focusedId"
:overlaid="modalOpened"
:filters="filters"
v-on:update="updateToot"
v-on:delete="deleteToot"
@focusNext="focusNext"
@ -30,7 +31,7 @@ import { Event } from '~/src/renderer/components/event'
export default {
name: 'posts-and-replies',
props: ['account', 'buffer'],
props: ['account', 'buffer', 'filters'],
components: { Toot },
data() {
return {

View File

@ -5,6 +5,7 @@
:message="message"
:focused="message.uri + message.id === focusedId"
:overlaid="modalOpened"
:filters="filters"
v-on:update="updateAncestorsToot"
v-on:delete="deleteAncestorsToot"
@focusNext="focusNext"
@ -19,6 +20,7 @@
:message="message"
:focused="message.uri + message.id === focusedId"
:overlaid="modalOpened"
:filters="filters"
:detailed="true"
v-on:update="updateToot"
v-on:delete="deleteToot"
@ -34,6 +36,7 @@
:message="message"
:focused="message.uri + message.id === focusedId"
:overlaid="modalOpened"
:filters="filters"
v-on:update="updateDescendantsToot"
v-on:delete="deleteDescendantsToot"
@focusNext="focusNext"
@ -66,6 +69,7 @@ export default {
descendants: state => state.descendants,
timeline: state => state.ancestors.concat([state.message]).concat(state.descendants)
}),
...mapGetters('TimelineSpace/Contents/SideBar/TootDetail', ['filters']),
...mapGetters('TimelineSpace/Modals', ['modalOpened'])
},
created() {
@ -74,13 +78,13 @@ export default {
mounted() {
Event.$on('focus-sidebar', () => {
this.focusedId = 0
this.$nextTick(function() {
this.$nextTick(function () {
this.focusedId = this.timeline[0].uri + this.timeline[0].id
})
})
},
watch: {
message: function() {
message: function () {
this.load()
}
},

View File

@ -3,7 +3,7 @@
<favourite
v-if="message.type === 'favourite'"
:message="message"
:filter="filter"
:filters="filters"
:focused="focused"
:overlaid="overlaid"
@focusNext="$emit('focusNext')"
@ -37,7 +37,7 @@
<mention
v-else-if="message.type === 'mention'"
:message="message"
:filter="filter"
:filters="filters"
:focused="focused"
:overlaid="overlaid"
v-on:update="updateToot"
@ -51,7 +51,7 @@
<quote
v-else-if="message.type === 'reblog' && message.status.quote"
:message="message"
:filter="filter"
:filters="filters"
:focused="focused"
:overlaid="overlaid"
@focusNext="$emit('focusNext')"
@ -63,7 +63,7 @@
<reblog
v-else-if="message.type === 'reblog' && !message.status.quote"
:message="message"
:filter="filter"
:filters="filters"
:focused="focused"
:overlaid="overlaid"
@focusNext="$emit('focusNext')"
@ -75,7 +75,7 @@
<reaction
v-else-if="message.type === 'emoji_reaction'"
:message="message"
:filter="filter"
:filters="filters"
:focused="focused"
:overlaid="overlaid"
@focusNext="$emit('focusNext')"
@ -87,7 +87,7 @@
<status
v-else-if="message.type === 'status'"
:message="message"
:filter="filter"
:filters="filters"
:focused="focused"
:overlaid="overlaid"
@focusNext="$emit('focusNext')"
@ -99,7 +99,7 @@
<PollVote
v-else-if="message.type === 'poll_vote'"
:message="message"
:filter="filter"
:filters="filters"
:focused="focused"
:overlaid="overlaid"
@focusNext="$emit('focusNext')"
@ -111,7 +111,7 @@
<PollExpired
v-else-if="message.type === 'poll_expired'"
:message="message"
:filter="filter"
:filters="filters"
:focused="focused"
:overlaid="overlaid"
@focusNext="$emit('focusNext')"
@ -142,9 +142,9 @@ export default {
type: Object,
default: {}
},
filter: {
type: String,
default: ''
filters: {
type: Array,
default: []
},
focused: {
type: Boolean,

View File

@ -112,6 +112,7 @@ import emojify from '~/src/renderer/utils/emojify'
import TimeFormat from '~/src/constants/timeFormat'
import FailoverImg from '~/src/renderer/components/atoms/FailoverImg'
import LinkPreview from '~/src/renderer/components/molecules/Toot/LinkPreview'
import Filtered from '@/utils/filter'
export default {
name: 'favourite',
@ -124,9 +125,9 @@ export default {
type: Object,
default: {}
},
filter: {
type: String,
default: ''
filters: {
type: Array,
default: []
},
focused: {
type: Boolean,
@ -233,7 +234,7 @@ export default {
return message.media_attachments
},
filtered(message) {
return this.filter.length > 0 && message.status.content.search(this.filter) >= 0
return Filtered(message.status.content, this.filters)
},
spoilered(message) {
return message.spoiler_text.length > 0

View File

@ -2,7 +2,7 @@
<div class="mention">
<toot
:message="message.status"
:filter="filter"
:filters="filters"
:focused="focused"
:overlaid="overlaid"
v-on:update="updateToot"
@ -11,7 +11,7 @@
@focusPrev="$emit('focusPrev')"
@focusRight="$emit('focusRight')"
@selectToot="$emit('select')"
>
>
</toot>
</div>
</template>
@ -26,9 +26,9 @@ export default {
type: Object,
default: {}
},
filter: {
type: String,
default: ''
filters: {
type: Array,
default: []
},
focused: {
type: Boolean,
@ -41,10 +41,10 @@ export default {
},
components: { Toot },
methods: {
updateToot (message) {
updateToot(message) {
return this.$emit('update', message)
},
deleteToot (message) {
deleteToot(message) {
return this.$emit('delete', message)
}
}

View File

@ -112,6 +112,7 @@ import emojify from '~/src/renderer/utils/emojify'
import TimeFormat from '~/src/constants/timeFormat'
import FailoverImg from '~/src/renderer/components/atoms/FailoverImg'
import LinkPreview from '~/src/renderer/components/molecules/Toot/LinkPreview'
import Filtered from '@/utils/filter'
export default {
name: 'poll-expired',
@ -124,9 +125,9 @@ export default {
type: Object,
default: {}
},
filter: {
type: String,
default: ''
filters: {
type: Array,
default: []
},
focused: {
type: Boolean,
@ -233,7 +234,7 @@ export default {
return message.media_attachments
},
filtered(message) {
return this.filter.length > 0 && message.status.content.search(this.filter) >= 0
return Filtered(message.status.content, this.filters)
},
spoilered(message) {
return message.spoiler_text.length > 0

View File

@ -112,6 +112,7 @@ import emojify from '~/src/renderer/utils/emojify'
import TimeFormat from '~/src/constants/timeFormat'
import FailoverImg from '~/src/renderer/components/atoms/FailoverImg'
import LinkPreview from '~/src/renderer/components/molecules/Toot/LinkPreview'
import Filtered from '@/utils/filter'
export default {
name: 'poll-vote',
@ -124,9 +125,9 @@ export default {
type: Object,
default: {}
},
filter: {
type: String,
default: ''
filters: {
type: Array,
default: []
},
focused: {
type: Boolean,
@ -233,7 +234,7 @@ export default {
return message.media_attachments
},
filtered(message) {
return this.filter.length > 0 && message.status.content.search(this.filter) >= 0
return Filtered(message.status.content, this.filters)
},
spoilered(message) {
return message.spoiler_text.length > 0

View File

@ -9,9 +9,7 @@
role="article"
aria-label="quoted toot"
>
<div v-show="filtered(message)" class="filtered">
Filtered
</div>
<div v-show="filtered(message)" class="filtered">Filtered</div>
<div v-show="!filtered(message)" class="quoted">
<div class="action">
<div class="action-mark">
@ -125,6 +123,7 @@ import TimeFormat from '~/src/constants/timeFormat'
import FailoverImg from '~/src/renderer/components/atoms/FailoverImg'
import LinkPreview from '~/src/renderer/components/molecules/Toot/LinkPreview'
import Quote from '@/components/molecules/Toot/Quote'
import Filtered from '@/utils/filter'
export default {
name: 'quote',
@ -138,9 +137,9 @@ export default {
type: Object,
default: {}
},
filter: {
type: String,
default: ''
filters: {
type: Array,
default: []
},
focused: {
type: Boolean,
@ -245,7 +244,7 @@ export default {
return message.media_attachments
},
filtered(message) {
return this.filter.length > 0 && message.status.content.search(this.filter) >= 0
return Filtered(message.status.content, this.filters)
},
spoilered(message) {
return message.spoiler_text.length > 0

View File

@ -9,9 +9,7 @@
role="article"
aria-label="reacted toot"
>
<div v-show="filtered(message)" class="filtered">
Filtered
</div>
<div v-show="filtered(message)" class="filtered">Filtered</div>
<div v-show="!filtered(message)" class="favourite">
<div class="action">
<div class="action-mark">
@ -114,6 +112,7 @@ import emojify from '~/src/renderer/utils/emojify'
import TimeFormat from '~/src/constants/timeFormat'
import FailoverImg from '~/src/renderer/components/atoms/FailoverImg'
import LinkPreview from '~/src/renderer/components/molecules/Toot/LinkPreview'
import Filtered from '@/utils/filter'
export default {
name: 'reaction',
@ -126,9 +125,9 @@ export default {
type: Object,
default: {}
},
filter: {
type: String,
default: ''
filters: {
type: Array,
default: []
},
focused: {
type: Boolean,
@ -235,7 +234,7 @@ export default {
return message.media_attachments
},
filtered(message) {
return this.filter.length > 0 && message.status.content.search(this.filter) >= 0
return Filtered(message.status.content, this.filters)
},
spoilered(message) {
return message.spoiler_text.length > 0

View File

@ -9,9 +9,7 @@
role="article"
aria-label="reblogged toot"
>
<div v-show="filtered(message)" class="filtered">
Filtered
</div>
<div v-show="filtered(message)" class="filtered">Filtered</div>
<div v-show="!filtered(message)" class="reblog">
<div class="action">
<div class="action-mark">
@ -116,6 +114,7 @@ import emojify from '~/src/renderer/utils/emojify'
import TimeFormat from '~/src/constants/timeFormat'
import FailoverImg from '~/src/renderer/components/atoms/FailoverImg'
import LinkPreview from '~/src/renderer/components/molecules/Toot/LinkPreview'
import Filtered from '@/utils/filter'
export default {
name: 'reblog',
@ -128,9 +127,9 @@ export default {
type: Object,
default: {}
},
filter: {
type: String,
default: ''
filters: {
type: Array,
default: []
},
focused: {
type: Boolean,
@ -235,7 +234,7 @@ export default {
return message.media_attachments
},
filtered(message) {
return this.filter.length > 0 && message.status.content.search(this.filter) >= 0
return Filtered(message.status.content, this.filters)
},
spoilered(message) {
return message.spoiler_text.length > 0

View File

@ -15,7 +15,7 @@
<div class="clearfix"></div>
<toot
:message="message.status"
:filter="filter"
:filters="filters"
:focused="focused"
:overlaid="overlaid"
v-on:update="updateToot"
@ -42,9 +42,9 @@ export default {
type: Object,
default: {}
},
filter: {
type: String,
default: ''
filters: {
type: Array,
default: []
},
focused: {
type: Boolean,

View File

@ -269,6 +269,7 @@ import LinkPreview from '~/src/renderer/components/molecules/Toot/LinkPreview'
import Quote from '@/components/molecules/Toot/Quote'
import { setInterval, clearInterval } from 'timers'
import QuoteSupported from '@/utils/quoteSupported'
import Filtered from '@/utils/filter'
export default {
name: 'toot',
@ -297,9 +298,9 @@ export default {
type: Object,
default: {}
},
filter: {
type: String,
default: ''
filters: {
type: Array,
default: []
},
focused: {
type: Boolean,
@ -389,7 +390,7 @@ export default {
return !this.sensitive || this.showAttachments
},
filtered: function () {
return this.filter.length > 0 && this.originalMessage.content.search(this.filter) >= 0
return Filtered(this.originalMessage.content, this.filters)
},
locked: function () {
return this.message.visibility === 'private'

View File

@ -22,6 +22,7 @@ export type TimelineSpaceState = {
tootMax: number
unreadNotification: UnreadNotification
sns: 'mastodon' | 'pleroma' | 'misskey'
filters: Array<Entity.Filter>
}
export const blankAccount: LocalAccount = {
@ -49,7 +50,8 @@ const state = (): TimelineSpaceState => ({
local: unreadSettings.Local.default,
public: unreadSettings.Public.default
},
sns: 'mastodon'
sns: 'mastodon',
filters: []
})
export const MUTATION_TYPES = {
@ -59,7 +61,8 @@ export const MUTATION_TYPES = {
UPDATE_EMOJIS: 'updateEmojis',
UPDATE_TOOT_MAX: 'updateTootMax',
UPDATE_UNREAD_NOTIFICATION: 'updateUnreadNotification',
CHANGE_SNS: 'changeSNS'
CHANGE_SNS: 'changeSNS',
UPDATE_FILTERS: 'updateFilters'
}
const mutations: MutationTree<TimelineSpaceState> = {
@ -87,6 +90,9 @@ const mutations: MutationTree<TimelineSpaceState> = {
},
[MUTATION_TYPES.CHANGE_SNS]: (state, sns: 'mastodon' | 'pleroma' | 'misskey') => {
state.sns = sns
},
[MUTATION_TYPES.UPDATE_FILTERS]: (state, filters: Array<Entity.Filter>) => {
state.filters = filters
}
}
@ -104,6 +110,7 @@ const actions: ActionTree<TimelineSpaceState, RootState> = {
dispatch('TimelineSpace/SideMenu/fetchFollowRequests', account, { root: true })
dispatch('TimelineSpace/SideMenu/confirmTimelines', account, { root: true })
await dispatch('loadUnreadNotification', accountId)
await dispatch('fetchFilters')
commit(MUTATION_TYPES.CHANGE_LOADING, false)
await dispatch('fetchContentsTimelines').catch(_ => {
throw new TimelineFetchError()
@ -175,6 +182,15 @@ const actions: ActionTree<TimelineSpaceState, RootState> = {
commit(MUTATION_TYPES.UPDATE_EMOJIS, res.data)
return res.data
},
/**
* fetchFilters
*/
fetchFilters: async ({ commit, state, rootState }): Promise<Array<Entity.Filter>> => {
const client = generator(state.sns, state.account.baseURL, state.account.accessToken, rootState.App.userAgent)
const res = await client.getFilters()
commit(MUTATION_TYPES.UPDATE_FILTERS, res.data)
return res.data
},
/**
* fetchInstance
*/

View File

@ -1,5 +1,5 @@
import generator, { Entity } from 'megalodon'
import { Module, MutationTree, ActionTree } from 'vuex'
import generator, { Entity, FilterContext } from 'megalodon'
import { Module, MutationTree, ActionTree, GetterTree } from 'vuex'
import { RootState } from '@/store'
export type HomeState = {
@ -138,11 +138,18 @@ const actions: ActionTree<HomeState, RootState> = {
}
}
const getters: GetterTree<HomeState, RootState> = {
filters: (_state, _getters, rootState) => {
return rootState.TimelineSpace.filters.filter(f => f.context.includes(FilterContext.Home) && !f.irreversible)
}
}
const Home: Module<HomeState, RootState> = {
namespaced: true,
state: state,
mutations: mutations,
actions: actions
actions: actions,
getters: getters
}
export default Home

View File

@ -1,4 +1,4 @@
import generator, { Entity, NotificationType } from 'megalodon'
import generator, { Entity, FilterContext, NotificationType } from 'megalodon'
import { Module, MutationTree, ActionTree, GetterTree } from 'vuex'
import { RootState } from '@/store'
import { MyWindow } from '~/src/types/global'
@ -156,6 +156,9 @@ const getters: GetterTree<NotificationsState, RootState> = {
return false
}
})
},
filters: (_state, _getters, rootState) => {
return rootState.TimelineSpace.filters.filter(f => f.context.includes(FilterContext.Notifications) && !f.irreversible)
}
}

View File

@ -1,5 +1,5 @@
import generator, { Entity } from 'megalodon'
import { Module, MutationTree, ActionTree } from 'vuex'
import generator, { Entity, FilterContext } from 'megalodon'
import { Module, MutationTree, ActionTree, GetterTree } from 'vuex'
import { RootState } from '@/store'
export type PublicState = {
@ -131,11 +131,18 @@ const actions: ActionTree<PublicState, RootState> = {
}
}
const getters: GetterTree<PublicState, RootState> = {
filters: (_state, _getters, rootState) => {
return rootState.TimelineSpace.filters.filter(f => f.context.includes(FilterContext.Public) && !f.irreversible)
}
}
const Public: Module<PublicState, RootState> = {
namespaced: true,
state: state,
mutations: mutations,
actions: actions
actions: actions,
getters: getters
}
export default Public

View File

@ -1,9 +1,10 @@
import { Module } from 'vuex'
import { GetterTree, Module } from 'vuex'
import { RootState } from '@/store'
import Posts, { PostsState } from './Timeline/Posts'
import PostsAndReplies, { PostsAndRepliesState } from './Timeline/PostsAndReplies'
import Media, { MediaState } from './Timeline/Media'
import { FilterContext } from 'megalodon'
export type TimelineState = {}
@ -17,6 +18,12 @@ export type TimelineModuleState = TimelineModule & TimelineState
const state = (): TimelineState => ({})
const getters: GetterTree<TimelineState, RootState> = {
filters: (_state, _getters, rootState) => {
return rootState.TimelineSpace.filters.filter(f => f.context.includes(FilterContext.Account) && !f.irreversible)
}
}
const Timeline: Module<TimelineState, RootState> = {
namespaced: true,
modules: {
@ -24,7 +31,8 @@ const Timeline: Module<TimelineState, RootState> = {
PostsAndReplies,
Media
},
state: state
state: state,
getters: getters
}
export default Timeline

View File

@ -1,5 +1,5 @@
import generator, { Entity } from 'megalodon'
import { Module, MutationTree, ActionTree } from 'vuex'
import generator, { Entity, FilterContext } from 'megalodon'
import { Module, MutationTree, ActionTree, GetterTree } from 'vuex'
import { RootState } from '@/store'
export type TootDetailState = {
@ -136,11 +136,18 @@ const actions: ActionTree<TootDetailState, RootState> = {
}
}
const getters: GetterTree<TootDetailState, RootState> = {
filters: (_state, _getters, rootState) => {
return rootState.TimelineSpace.filters.filter(f => f.context.includes(FilterContext.Thread) && !f.irreversible)
}
}
const TootDetail: Module<TootDetailState, RootState> = {
namespaced: true,
state: state,
mutations: mutations,
actions: actions
actions: actions,
getters: getters
}
export default TootDetail

View File

@ -0,0 +1,38 @@
import { Entity } from 'megalodon'
// refs: https://github.com/tootsuite/mastodon/blob/c3aef491d66aec743a3a53e934a494f653745b61/app/javascript/mastodon/selectors/index.js#L43
const filtered = (status: string, filters: Array<Entity.Filter>): boolean => {
if (filters.length === 0) {
return false
}
const regexp = filterRegexp(filters)
return status.match(regexp) !== null
}
const filterRegexp = (filters: Array<Entity.Filter>): RegExp => {
return new RegExp(
filters
.map(f => {
let exp = escapeRegExp(f.phrase)
if (f.whole_word) {
if (/^[\w]/.test(exp)) {
exp = `\\b${exp}`
}
if (/[\w]$/.test(exp)) {
exp = `${exp}\\b`
}
}
return exp
})
.join('|'),
'i'
)
}
const escapeRegExp = (str: string) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
export default filtered