refs #2258 Apply filter to home timeline
This commit is contained in:
parent
c9d860465c
commit
23f2c95ef9
@ -84,7 +84,8 @@ const state = (): TimelineSpaceState => {
|
||||
local: true,
|
||||
public: true
|
||||
},
|
||||
sns: 'mastodon'
|
||||
sns: 'mastodon',
|
||||
filters: []
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -16,7 +16,8 @@ describe('TimelineSpace', () => {
|
||||
local: unreadSettings.Local.default,
|
||||
public: unreadSettings.Public.default
|
||||
},
|
||||
sns: 'mastodon'
|
||||
sns: 'mastodon',
|
||||
filters: []
|
||||
}
|
||||
})
|
||||
|
||||
|
106
spec/renderer/unit/utils/filter.spec.ts
Normal file
106
spec/renderer/unit/utils/filter.spec.ts
Normal 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()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
@ -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
|
||||
|
@ -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'
|
||||
|
@ -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
|
||||
*/
|
||||
|
@ -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
|
||||
|
38
src/renderer/utils/filter.ts
Normal file
38
src/renderer/utils/filter.ts
Normal 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
|
Loading…
x
Reference in New Issue
Block a user