fix: fix bell notifications, add tests

This commit is contained in:
Nolan Lawson 2022-04-28 08:18:50 -07:00
parent 2e9afd711f
commit c67be9acc2
21 changed files with 177 additions and 152 deletions

View file

@ -1,6 +1,7 @@
export default [ export default [
{ id: 'pinafore-logo', src: 'src/static/sailboat.svg', inline: true }, { id: 'pinafore-logo', src: 'src/static/sailboat.svg', inline: true },
{ id: 'fa-bell', src: 'src/thirdparty/font-awesome-svg-png/white/svg/bell.svg', inline: true }, { id: 'fa-bell', src: 'src/thirdparty/font-awesome-svg-png/white/svg/bell.svg', inline: true },
{ id: 'fa-bell-o', src: 'src/thirdparty/font-awesome-svg-png/white/svg/bell-o.svg' },
{ id: 'fa-users', src: 'src/thirdparty/font-awesome-svg-png/white/svg/users.svg', inline: true }, { id: 'fa-users', src: 'src/thirdparty/font-awesome-svg-png/white/svg/users.svg', inline: true },
{ id: 'fa-globe', src: 'src/thirdparty/font-awesome-svg-png/white/svg/globe.svg' }, { id: 'fa-globe', src: 'src/thirdparty/font-awesome-svg-png/white/svg/globe.svg' },
{ id: 'fa-gear', src: 'src/thirdparty/font-awesome-svg-png/white/svg/gear.svg', inline: true }, { id: 'fa-gear', src: 'src/thirdparty/font-awesome-svg-png/white/svg/gear.svg', inline: true },
@ -22,6 +23,7 @@ export default [
{ id: 'fa-user-plus', src: 'src/thirdparty/font-awesome-svg-png/white/svg/user-plus.svg' }, { id: 'fa-user-plus', src: 'src/thirdparty/font-awesome-svg-png/white/svg/user-plus.svg' },
{ id: 'fa-external-link', src: 'src/thirdparty/font-awesome-svg-png/white/svg/external-link.svg' }, { id: 'fa-external-link', src: 'src/thirdparty/font-awesome-svg-png/white/svg/external-link.svg' },
{ id: 'fa-search', src: 'src/thirdparty/font-awesome-svg-png/white/svg/search.svg', inline: true }, { id: 'fa-search', src: 'src/thirdparty/font-awesome-svg-png/white/svg/search.svg', inline: true },
{ id: 'fa-comment', src: 'src/thirdparty/font-awesome-svg-png/white/svg/comment.svg' },
{ id: 'fa-comments', src: 'src/thirdparty/font-awesome-svg-png/white/svg/comments.svg', inline: true }, { id: 'fa-comments', src: 'src/thirdparty/font-awesome-svg-png/white/svg/comments.svg', inline: true },
{ id: 'fa-paperclip', src: 'src/thirdparty/font-awesome-svg-png/white/svg/paperclip.svg' }, { id: 'fa-paperclip', src: 'src/thirdparty/font-awesome-svg-png/white/svg/paperclip.svg' },
{ id: 'fa-thumb-tack', src: 'src/thirdparty/font-awesome-svg-png/white/svg/thumb-tack.svg' }, { id: 'fa-thumb-tack', src: 'src/thirdparty/font-awesome-svg-png/white/svg/thumb-tack.svg' },

View file

@ -309,10 +309,10 @@ export default {
true {(follow requested)} true {(follow requested)}
other {} other {}
}`, }`,
notifyLabel: `Subscribe`, notify: 'Subscribe to {account}',
denotifyLabel: `Unsubscribe`, denotify: 'Unsubscribe from {account}',
subscribedAccount: 'Subscribed to account', subscribedAccount: 'Subscribed to account',
unsubscribedAccount: 'Unsubscribed to account', unsubscribedAccount: 'Unsubscribed from account',
unblock: 'Unblock', unblock: 'Unblock',
nameAndFollowing: 'Name and following', nameAndFollowing: 'Name and following',
clickToSeeAvatar: 'Click to see avatar', clickToSeeAvatar: 'Click to see avatar',
@ -474,6 +474,7 @@ export default {
newFollowers: 'New followers', newFollowers: 'New followers',
reblogs: 'Boosts', reblogs: 'Boosts',
pollResults: 'Poll results', pollResults: 'Poll results',
subscriptions: 'Subscribed toots',
needToReauthenticate: 'You need to reauthenticate in order to enable push notification. Log out of {instance}?', needToReauthenticate: 'You need to reauthenticate in order to enable push notification. Log out of {instance}?',
failedToUpdatePush: 'Failed to update push notification settings: {error}', failedToUpdatePush: 'Failed to update push notification settings: {error}',
// Themes // Themes
@ -508,6 +509,7 @@ export default {
rebloggedYou: 'boosted your toot', rebloggedYou: 'boosted your toot',
favoritedYou: 'favorited your toot', favoritedYou: 'favorited your toot',
followedYou: 'followed you', followedYou: 'followed you',
posted: 'posted',
pollYouCreatedEnded: 'A poll you created has ended', pollYouCreatedEnded: 'A poll you created has ended',
pollYouVotedEnded: 'A poll you voted on has ended', pollYouVotedEnded: 'A poll you voted on has ended',
reblogged: 'boosted', reblogged: 'boosted',

View file

@ -4,7 +4,7 @@ import { toast } from '../_components/toast/toast.js'
import { updateLocalRelationship } from './accounts.js' import { updateLocalRelationship } from './accounts.js'
import { formatIntl } from '../_utils/formatIntl.js' import { formatIntl } from '../_utils/formatIntl.js'
export async function setAccountNotify (accountId, notify, toastOnSuccess) { export async function setAccountNotified (accountId, notify, toastOnSuccess) {
const { currentInstance, accessToken } = store.get() const { currentInstance, accessToken } = store.get()
try { try {
let relationship let relationship
@ -15,11 +15,11 @@ export async function setAccountNotify (accountId, notify, toastOnSuccess) {
} }
await updateLocalRelationship(currentInstance, accountId, relationship) await updateLocalRelationship(currentInstance, accountId, relationship)
if (toastOnSuccess) { if (toastOnSuccess) {
/* no await */ toast.say(follow ? 'intl.subscribedAccount' : 'intl.unsubscribedAccount') /* no await */ toast.say(notify ? 'intl.subscribedAccount' : 'intl.unsubscribedAccount')
} }
} catch (e) { } catch (e) {
console.error(e) console.error(e)
/* no await */ toast.say(follow /* no await */ toast.say(notify
? formatIntl('intl.unableToSubscribe', { error: (e.message || '') }) ? formatIntl('intl.unableToSubscribe', { error: (e.message || '') })
: formatIntl('intl.unableToUnsubscribe', { error: (e.message || '') }) : formatIntl('intl.unableToUnsubscribe', { error: (e.message || '') })
) )

View file

@ -4,13 +4,13 @@ import { auth, basename } from './utils.js'
export async function notifyAccount (instanceName, accessToken, accountId) { export async function notifyAccount (instanceName, accessToken, accountId) {
const url = `${basename(instanceName)}/api/v1/accounts/${accountId}/follow` const url = `${basename(instanceName)}/api/v1/accounts/${accountId}/follow`
return post(url, { return post(url, {
"notify": true, notify: true
}, auth(accessToken), { timeout: WRITE_TIMEOUT }) }, auth(accessToken), { timeout: WRITE_TIMEOUT })
} }
export async function denotifyAccount (instanceName, accessToken, accountId) { export async function denotifyAccount (instanceName, accessToken, accountId) {
const url = `${basename(instanceName)}/api/v1/accounts/${accountId}/follow` const url = `${basename(instanceName)}/api/v1/accounts/${accountId}/follow`
return post(url, { return post(url, {
"notify": false notify: false
}, auth(accessToken), { timeout: WRITE_TIMEOUT }) }, auth(accessToken), { timeout: WRITE_TIMEOUT })
} }

View file

@ -23,6 +23,7 @@ import { composeNewStatusMentioning } from '../../../_actions/mention.js'
import { toggleMute } from '../../../_actions/toggleMute.js' import { toggleMute } from '../../../_actions/toggleMute.js'
import { reportStatusOrAccount } from '../../../_actions/report.js' import { reportStatusOrAccount } from '../../../_actions/report.js'
import { formatIntl } from '../../../_utils/formatIntl.js' import { formatIntl } from '../../../_utils/formatIntl.js'
import { setAccountNotified } from '../../../_actions/setAccountNotified.js'
export default { export default {
oncreate, oncreate,
@ -39,6 +40,7 @@ export default {
username: ({ account }) => account.username, username: ({ account }) => account.username,
muting: ({ relationship }) => relationship && relationship.muting, muting: ({ relationship }) => relationship && relationship.muting,
blocking: ({ relationship }) => relationship && relationship.blocking, blocking: ({ relationship }) => relationship && relationship.blocking,
notifying: ({ relationship }) => relationship && relationship.notifying,
followLabel: ({ following, followRequested, account, username }) => { followLabel: ({ following, followRequested, account, username }) => {
if (typeof following === 'undefined' || !account) { if (typeof following === 'undefined' || !account) {
return '' return ''
@ -86,7 +88,7 @@ export default {
blockLabel, blocking, blockIcon, muteLabel, muteIcon, blockLabel, blocking, blockIcon, muteLabel, muteIcon,
followLabel, followIcon, following, followRequested, followLabel, followIcon, following, followRequested,
accountId, verifyCredentialsId, username, isUser, showReblogsLabel, accountId, verifyCredentialsId, username, isUser, showReblogsLabel,
domain, blockDomainLabel, reportLabel domain, blockDomainLabel, reportLabel, notifying
}) => ([ }) => ([
!isUser && { !isUser && {
key: 'mention', key: 'mention',
@ -98,6 +100,16 @@ export default {
label: followLabel, label: followLabel,
icon: followIcon icon: followIcon
}, },
!isUser && following && notifying === false && { // notifying could be undefined for old servers
key: 'notify',
label: formatIntl('intl.notify', { account: `@${username}` }),
icon: '#fa-bell'
},
!isUser && following && notifying === true && { // notifying could be undefined for old servers
key: 'denotify',
label: formatIntl('intl.denotify', { account: `@${username}` }),
icon: '#fa-bell-o'
},
!isUser && { !isUser && {
key: 'block', key: 'block',
label: blockLabel, label: blockLabel,
@ -151,6 +163,10 @@ export default {
return this.onCopyClicked() return this.onCopyClicked()
case 'report': case 'report':
return this.onReport() return this.onReport()
case 'notify':
return this.onNotifyClicked()
case 'denotify':
return this.onDenotifyClicked()
} }
}, },
async onMentionClicked () { async onMentionClicked () {
@ -193,6 +209,16 @@ export default {
const { account } = this.get() const { account } = this.get()
this.close() this.close()
await reportStatusOrAccount({ account }) await reportStatusOrAccount({ account })
},
async onNotifyClicked () {
const { accountId } = this.get()
this.close()
await setAccountNotified(accountId, /* notify */ true, /* toastOnSuccess */ true)
},
async onDenotifyClicked () {
const { accountId } = this.get()
this.close()
await setAccountNotified(accountId, /* notify */ false, /* toastOnSuccess */ true)
} }
}, },
components: { components: {

View file

@ -26,6 +26,7 @@ import { shareStatus } from '../../../_actions/share.js'
import { toggleMute } from '../../../_actions/toggleMute.js' import { toggleMute } from '../../../_actions/toggleMute.js'
import { reportStatusOrAccount } from '../../../_actions/report.js' import { reportStatusOrAccount } from '../../../_actions/report.js'
import { formatIntl } from '../../../_utils/formatIntl.js' import { formatIntl } from '../../../_utils/formatIntl.js'
import { setAccountNotified } from '../../../_actions/setAccountNotified.js'
export default { export default {
oncreate, oncreate,
@ -53,6 +54,7 @@ export default {
username: ({ account }) => account.username, username: ({ account }) => account.username,
muting: ({ relationship }) => relationship.muting, muting: ({ relationship }) => relationship.muting,
blocking: ({ relationship }) => relationship.blocking, blocking: ({ relationship }) => relationship.blocking,
notifying: ({ relationship }) => relationship && relationship.notifying,
followLabel: ({ following, followRequested, account, username }) => { followLabel: ({ following, followRequested, account, username }) => {
if (typeof following === 'undefined' || !account) { if (typeof following === 'undefined' || !account) {
return '' return ''
@ -96,7 +98,8 @@ export default {
items: ({ items: ({
blockLabel, blocking, blockIcon, muteLabel, muteIcon, followLabel, followIcon, blockLabel, blocking, blockIcon, muteLabel, muteIcon, followLabel, followIcon,
following, followRequested, pinLabel, isUser, visibility, mentionsUser, mutingConversation, following, followRequested, pinLabel, isUser, visibility, mentionsUser, mutingConversation,
muteConversationLabel, muteConversationIcon, supportsWebShare, isPublicOrUnlisted, bookmarkLabel muteConversationLabel, muteConversationIcon, supportsWebShare, isPublicOrUnlisted, bookmarkLabel,
username, notifying
}) => ([ }) => ([
isUser && { isUser && {
key: 'delete', key: 'delete',
@ -113,6 +116,16 @@ export default {
label: followLabel, label: followLabel,
icon: followIcon icon: followIcon
}, },
!isUser && following && notifying === false && { // notifying could be undefined for old servers
key: 'notify',
label: formatIntl('intl.notify', { account: `@${username}` }),
icon: '#fa-bell'
},
!isUser && following && notifying === true && { // notifying could be undefined for old servers
key: 'denotify',
label: formatIntl('intl.denotify', { account: `@${username}` }),
icon: '#fa-bell-o'
},
!isUser && { !isUser && {
key: 'block', key: 'block',
label: blockLabel, label: blockLabel,
@ -187,6 +200,10 @@ export default {
return this.onReport() return this.onReport()
case 'bookmark': case 'bookmark':
return this.onBookmark() return this.onBookmark()
case 'notify':
return this.onNotifyClicked()
case 'denotify':
return this.onDenotifyClicked()
} }
}, },
async onDeleteClicked () { async onDeleteClicked () {
@ -244,6 +261,16 @@ export default {
const { status } = this.get() const { status } = this.get()
this.close() this.close()
await setStatusBookmarkedOrUnbookmarked(status.id, !status.bookmarked) await setStatusBookmarkedOrUnbookmarked(status.id, !status.bookmarked)
},
async onNotifyClicked () {
const { accountId } = this.get()
this.close()
await setAccountNotified(accountId, /* notify */ true, /* toastOnSuccess */ true)
},
async onDenotifyClicked () {
const { accountId } = this.get()
this.close()
await setAccountNotified(accountId, /* notify */ false, /* toastOnSuccess */ true)
} }
} }
} }

View file

@ -8,7 +8,6 @@
<div class="account-profile-grid-wrapper"> <div class="account-profile-grid-wrapper">
<div class="account-profile-grid"> <div class="account-profile-grid">
<AccountProfileHeader {account} {relationship} {verifyCredentials} /> <AccountProfileHeader {account} {relationship} {verifyCredentials} />
<AccountProfileNotify {account} {relationship} {verifyCredentials} />
<AccountProfileFollow {account} {relationship} {verifyCredentials} /> <AccountProfileFollow {account} {relationship} {verifyCredentials} />
<AccountProfileNote {account} /> <AccountProfileNote {account} />
<AccountProfileMeta {account} /> <AccountProfileMeta {account} />
@ -38,7 +37,7 @@
display: grid; display: grid;
grid-template-areas: "avatar name label followed-by follow" grid-template-areas: "avatar name label followed-by follow"
"avatar username username username follow" "avatar username username username follow"
"avatar note note note notify" "avatar note note note follow"
"meta meta meta meta meta" "meta meta meta meta meta"
"details details details details details"; "details details details details details";
grid-template-columns: min-content auto 1fr 1fr min-content; grid-template-columns: min-content auto 1fr 1fr min-content;
@ -72,7 +71,7 @@
grid-template-areas: "avatar name follow" grid-template-areas: "avatar name follow"
"avatar label follow" "avatar label follow"
"avatar username follow" "avatar username follow"
"avatar followed-by notify" "avatar followed-by follow"
"note note note" "note note note"
"meta meta meta" "meta meta meta"
"details details details"; "details details details";
@ -98,7 +97,6 @@
"username username" "username username"
"followed-by followed-by" "followed-by followed-by"
"follow follow" "follow follow"
"notify notify"
"note note" "note note"
"meta meta" "meta meta"
"details details"; "details details";
@ -110,7 +108,6 @@
</style> </style>
<script> <script>
import AccountProfileHeader from './AccountProfileHeader.html' import AccountProfileHeader from './AccountProfileHeader.html'
import AccountProfileNotify from './AccountProfileNotify.html'
import AccountProfileFollow from './AccountProfileFollow.html' import AccountProfileFollow from './AccountProfileFollow.html'
import AccountProfileNote from './AccountProfileNote.html' import AccountProfileNote from './AccountProfileNote.html'
import AccountProfileMeta from './AccountProfileMeta.html' import AccountProfileMeta from './AccountProfileMeta.html'
@ -147,7 +144,6 @@
AccountProfileHeader, AccountProfileHeader,
AccountProfileFollow, AccountProfileFollow,
AccountProfileNote, AccountProfileNote,
AccountProfileNotify,
AccountProfileMeta, AccountProfileMeta,
AccountProfileDetails, AccountProfileDetails,
AccountProfileMovedBanner, AccountProfileMovedBanner,

View file

@ -1,92 +0,0 @@
<div class="account-profile-notify {shown ? 'shown' : ''}">
<!--
The bell notification button (Mastodon 3.3+)
Shows if we're getting notifications or not.
It is not possible to turn on notifications for accounts you don't follow.
Also the instance can just have no support for this feature.
-->
<IconButton
className="account-profile-notify-icon-button"
{label}
pressedLabel="{intl.denotifyLabel}"
{href}
big={!$isVeryTinyMobileSize}
on:click="onNotifyButtonClick(event)"
ref:icon
/>
</div>
<style>
.account-profile-notify {
grid-area: notify;
align-self: flex-start;
display: none;
}
.account-profile-notify.shown {
display: block;
}
@media (max-width: 240px) {
.account-profile-notify {
justify-self: flex-end;
}
}
</style>
<script>
import IconButton from '../IconButton.html'
import { FOLLOW_BUTTON_ANIMATION } from '../../_static/animations.js'
import { store } from '../../_store/store.js'
import { setAccountNotify } from '../../_actions/notify.js'
import { formatIntl } from '../../_utils/formatIntl.js'
export default {
methods: {
oncreate () {
if (process.browser) {
window.__button = this
}
},
async onNotifyButtonClick (e) {
e.preventDefault()
e.stopPropagation()
const {
account,
accountId,
notifying
} = this.get()
if (notifying) { // unblock
await setAccountNotify(accountId, false)
} else { // follow/unfollow
this.refs.icon.animate(FOLLOW_BUTTON_ANIMATION)
await setAccountNotify(accountId, true)
}
}
},
store: () => store,
data: () => ({
}),
computed: {
accountId: ({ account }) => account.id,
notifying: ({ relationship }) => {
return relationship && relationship.notifying
},
href: ({ notifying }) => {
if (notifying) {
return '#fa-bell-ringing'
}
return '#fa-bell-o'
},
label: ({ notifying }) => {
if (notifying) {
return formatIntl('intl.notifyLabel')
}
return formatIntl('intl.denotifyLabel')
},
shown: ({ verifyCredentials, relationship }) => (
verifyCredentials && relationship && verifyCredentials.id !== relationship.id && relationship.following
),
},
components: {
IconButton
}
}
</script>

View file

@ -10,7 +10,8 @@
NOTIFICATION_FAVORITES, NOTIFICATION_FAVORITES,
NOTIFICATION_FOLLOWS, NOTIFICATION_FOLLOWS,
NOTIFICATION_MENTIONS, NOTIFICATION_MENTIONS,
NOTIFICATION_POLLS NOTIFICATION_POLLS,
NOTIFICATION_SUBSCRIPTIONS
} from '../../../_static/instanceSettings.js' } from '../../../_static/instanceSettings.js'
export default { export default {
@ -40,6 +41,11 @@
key: NOTIFICATION_POLLS, key: NOTIFICATION_POLLS,
label: 'intl.pollResults', label: 'intl.pollResults',
defaultValue: true defaultValue: true
},
{
key: NOTIFICATION_SUBSCRIPTIONS,
label: 'intl.subscriptions',
defaultValue: true
} }
] ]
}), }),

View file

@ -76,6 +76,10 @@
{ {
key: 'poll', key: 'poll',
label: 'intl.pollResults' label: 'intl.pollResults'
},
{
key: 'status',
label: 'intl.subscriptions'
} }
] ]
}), }),

View file

@ -295,7 +295,7 @@
) )
), ),
showHeader: ({ notification, status, timelineType }) => ( showHeader: ({ notification, status, timelineType }) => (
(notification && ['reblog', 'favourite', 'poll'].includes(notification.type)) || (notification && ['reblog', 'favourite', 'poll', 'status'].includes(notification.type)) ||
status.reblog || status.reblog ||
timelineType === 'pinned' timelineType === 'pinned'
), ),

View file

@ -125,6 +125,8 @@
return '#fa-user-plus' return '#fa-user-plus'
} else if (notificationType === 'poll') { } else if (notificationType === 'poll') {
return '#fa-bar-chart' return '#fa-bar-chart'
} else if (notificationType === 'status') {
return '#fa-comment'
} }
return '#fa-star' return '#fa-star'
}, },
@ -135,6 +137,8 @@
return 'intl.favoritedYou' return 'intl.favoritedYou'
} else if (notificationType === 'follow') { } else if (notificationType === 'follow') {
return 'intl.followedYou' return 'intl.followedYou'
} else if (notificationType === 'status') {
return 'intl.posted'
} else if (notificationType === 'poll') { } else if (notificationType === 'poll') {
if ($currentVerifyCredentials && status && $currentVerifyCredentials.id === status.account.id) { if ($currentVerifyCredentials && status && $currentVerifyCredentials.id === status.account.id) {
return 'intl.pollYouCreatedEnded' return 'intl.pollYouCreatedEnded'

View file

@ -6,3 +6,4 @@ export const NOTIFICATION_FAVORITES = 'notificationFavs'
export const NOTIFICATION_FOLLOWS = 'notificationFollows' export const NOTIFICATION_FOLLOWS = 'notificationFollows'
export const NOTIFICATION_MENTIONS = 'notificationMentions' export const NOTIFICATION_MENTIONS = 'notificationMentions'
export const NOTIFICATION_POLLS = 'notificationPolls' export const NOTIFICATION_POLLS = 'notificationPolls'
export const NOTIFICATION_SUBSCRIPTIONS = 'notificationSubscriptions'

View file

@ -3,7 +3,7 @@ import {
HOME_REPLIES, HOME_REPLIES,
NOTIFICATION_FAVORITES, NOTIFICATION_FAVORITES,
NOTIFICATION_FOLLOWS, NOTIFICATION_MENTIONS, NOTIFICATION_POLLS, NOTIFICATION_FOLLOWS, NOTIFICATION_MENTIONS, NOTIFICATION_POLLS,
NOTIFICATION_REBLOGS NOTIFICATION_REBLOGS, NOTIFICATION_SUBSCRIPTIONS
} from '../../_static/instanceSettings.js' } from '../../_static/instanceSettings.js'
import { import {
WORD_FILTER_CONTEXT_ACCOUNT, WORD_FILTER_CONTEXT_ACCOUNT,
@ -46,12 +46,14 @@ export function timelineFilterComputations (store) {
computeTimelineFilter(store, 'timelineShowFavs', { notifications: NOTIFICATION_FAVORITES }) computeTimelineFilter(store, 'timelineShowFavs', { notifications: NOTIFICATION_FAVORITES })
computeTimelineFilter(store, 'timelineShowMentions', { notifications: NOTIFICATION_MENTIONS }) computeTimelineFilter(store, 'timelineShowMentions', { notifications: NOTIFICATION_MENTIONS })
computeTimelineFilter(store, 'timelineShowPolls', { notifications: NOTIFICATION_POLLS }) computeTimelineFilter(store, 'timelineShowPolls', { notifications: NOTIFICATION_POLLS })
computeTimelineFilter(store, 'timelineShowSubscriptions', { notifications: NOTIFICATION_SUBSCRIPTIONS })
computeNotificationFilter(store, 'timelineNotificationShowReblogs', NOTIFICATION_REBLOGS) computeNotificationFilter(store, 'timelineNotificationShowReblogs', NOTIFICATION_REBLOGS)
computeNotificationFilter(store, 'timelineNotificationShowFollows', NOTIFICATION_FOLLOWS) computeNotificationFilter(store, 'timelineNotificationShowFollows', NOTIFICATION_FOLLOWS)
computeNotificationFilter(store, 'timelineNotificationShowFavs', NOTIFICATION_FAVORITES) computeNotificationFilter(store, 'timelineNotificationShowFavs', NOTIFICATION_FAVORITES)
computeNotificationFilter(store, 'timelineNotificationShowMentions', NOTIFICATION_MENTIONS) computeNotificationFilter(store, 'timelineNotificationShowMentions', NOTIFICATION_MENTIONS)
computeNotificationFilter(store, 'timelineNotificationShowPolls', NOTIFICATION_POLLS) computeNotificationFilter(store, 'timelineNotificationShowPolls', NOTIFICATION_POLLS)
computeNotificationFilter(store, 'timelineNotificationShowSubscriptions', NOTIFICATION_SUBSCRIPTIONS)
store.compute( store.compute(
'timelineWordFilterContext', 'timelineWordFilterContext',
@ -85,10 +87,10 @@ export function timelineFilterComputations (store) {
[ [
'timelineShowReblogs', 'timelineShowReplies', 'timelineShowFollows', 'timelineShowReblogs', 'timelineShowReplies', 'timelineShowFollows',
'timelineShowFavs', 'timelineShowMentions', 'timelineShowPolls', 'timelineShowFavs', 'timelineShowMentions', 'timelineShowPolls',
'timelineWordFilterContext' 'timelineShowSubscriptions', 'timelineWordFilterContext'
], ],
(showReblogs, showReplies, showFollows, showFavs, showMentions, showPolls, wordFilterContext) => ( (showReblogs, showReplies, showFollows, showFavs, showMentions, showPolls, showSubscriptions, wordFilterContext) => (
createFilterFunction(showReblogs, showReplies, showFollows, showFavs, showMentions, showPolls, wordFilterContext) createFilterFunction(showReblogs, showReplies, showFollows, showFavs, showMentions, showPolls, showSubscriptions, wordFilterContext)
) )
) )
@ -99,10 +101,10 @@ export function timelineFilterComputations (store) {
[ [
'timelineNotificationShowReblogs', 'timelineNotificationShowFollows', 'timelineNotificationShowReblogs', 'timelineNotificationShowFollows',
'timelineNotificationShowFavs', 'timelineNotificationShowMentions', 'timelineNotificationShowFavs', 'timelineNotificationShowMentions',
'timelineNotificationShowPolls' 'timelineNotificationShowPolls', 'timelineNotificationShowSubscriptions'
], ],
(showReblogs, showFollows, showFavs, showMentions, showPolls) => ( (showReblogs, showFollows, showFavs, showMentions, showPolls, showSubscriptions) => (
createFilterFunction(showReblogs, true, showFollows, showFavs, showMentions, showPolls, WORD_FILTER_CONTEXT_NOTIFICATIONS) createFilterFunction(showReblogs, true, showFollows, showFavs, showMentions, showPolls, showSubscriptions, WORD_FILTER_CONTEXT_NOTIFICATIONS)
) )
) )

View file

@ -1,7 +1,8 @@
// create a function for filtering timeline item summaries // create a function for filtering timeline item summaries
export const createFilterFunction = ( export const createFilterFunction = (
showReblogs, showReplies, showFollows, showFavs, showMentions, showPolls, wordFilterContext showReblogs, showReplies, showFollows, showFavs, showMentions, showPolls,
showSubscriptions, wordFilterContext
) => { ) => {
return item => { return item => {
if (item.filterContexts && item.filterContexts.includes(wordFilterContext)) { if (item.filterContexts && item.filterContexts.includes(wordFilterContext)) {
@ -19,6 +20,8 @@ export const createFilterFunction = (
return showMentions return showMentions
case 'follow': case 'follow':
return showFollows return showFollows
case 'status':
return showSubscriptions
} }
if (item.reblogId) { if (item.reblogId) {
return showReblogs return showReblogs

View file

@ -214,6 +214,7 @@ async function showRichNotification (data, notification) {
} }
case 'reblog': case 'reblog':
case 'favourite': case 'favourite':
case 'status':
case 'poll': { case 'poll': {
await self.registration.showNotification(data.title, { await self.registration.showNotification(data.title, {
badge, badge,

View file

@ -1,18 +1,18 @@
import { favoriteStatus } from '../src/routes/_api/favorite' import { favoriteStatus } from '../src/routes/_api/favorite.js'
import fetch from 'node-fetch' import fetch from 'node-fetch'
import FileApi from 'file-api' import FileApi from 'file-api'
import { users } from './users' import { users } from './users.js'
import { postStatus } from '../src/routes/_api/statuses' import { postStatus } from '../src/routes/_api/statuses.js'
import { deleteStatus } from '../src/routes/_api/delete' import { deleteStatus } from '../src/routes/_api/delete.js'
import { authorizeFollowRequest, getFollowRequests } from '../src/routes/_api/followRequests' import { authorizeFollowRequest, getFollowRequests } from '../src/routes/_api/followRequests.js'
import { followAccount, unfollowAccount } from '../src/routes/_api/follow' import { followAccount, unfollowAccount } from '../src/routes/_api/follow.js'
import { updateCredentials } from '../src/routes/_api/updateCredentials' import { updateCredentials } from '../src/routes/_api/updateCredentials.js'
import { reblogStatus } from '../src/routes/_api/reblog' import { reblogStatus } from '../src/routes/_api/reblog.js'
import { submitMedia } from './submitMedia.js' import { submitMedia } from './submitMedia.js'
import { voteOnPoll } from '../src/routes/_api/polls' import { voteOnPoll } from '../src/routes/_api/polls.js'
import { POLL_EXPIRY_DEFAULT } from '../src/routes/_static/polls' import { POLL_EXPIRY_DEFAULT } from '../src/routes/_static/polls.js'
import { createList, getLists } from '../src/routes/_api/lists' import { createList, getLists } from '../src/routes/_api/lists.js'
import { createFilter, deleteFilter, getFilters } from '../src/routes/_api/filters' import { createFilter, deleteFilter, getFilters } from '../src/routes/_api/filters.js'
global.fetch = fetch global.fetch = fetch
global.File = FileApi.File global.File = FileApi.File

View file

@ -2,7 +2,7 @@ import {
accountProfileFollowButton, accountProfileFollowButton,
accountProfileFollowedBy, accountProfileMoreOptionsButton, communityNavButton, getNthSearchResult, accountProfileFollowedBy, accountProfileMoreOptionsButton, communityNavButton, getNthSearchResult,
getNthStatus, getNthStatusOptionsButton, getNthDialogOptionsOption, getUrl, modalDialog, getNthStatus, getNthStatusOptionsButton, getNthDialogOptionsOption, getUrl, modalDialog,
sleep sleep, getDialogOptionWithText
} from '../utils' } from '../utils'
import { Selector as $ } from 'testcafe' import { Selector as $ } from 'testcafe'
import { loginAsFoobar } from '../roles' import { loginAsFoobar } from '../roles'
@ -21,10 +21,10 @@ test('Can block and unblock an account from a status', async t => {
await t await t
.click(getNthStatusOptionsButton(1)) .click(getNthStatusOptionsButton(1))
.expect(getNthDialogOptionsOption(1).innerText).contains('Unfollow @admin') .expect(getNthDialogOptionsOption(1).innerText).contains('Unfollow @admin')
.expect(getNthDialogOptionsOption(2).innerText).contains('Block @admin') .expect(getNthDialogOptionsOption(3).innerText).contains('Block @admin')
await sleep(500) await sleep(500)
await t await t
.click(getNthDialogOptionsOption(2)) .click(getDialogOptionWithText('Block @admin'))
.expect(modalDialog.exists).notOk() .expect(modalDialog.exists).notOk()
await sleep(500) await sleep(500)
await t await t
@ -60,12 +60,9 @@ test('Can block and unblock an account from the account profile page', async t =
await sleep(500) await sleep(500)
await t await t
.click(accountProfileMoreOptionsButton) .click(accountProfileMoreOptionsButton)
.expect(getNthDialogOptionsOption(1).innerText).contains('Mention @baz')
.expect(getNthDialogOptionsOption(2).innerText).contains('Follow @baz')
.expect(getNthDialogOptionsOption(3).innerText).contains('Block @baz')
await sleep(500) await sleep(500)
await t await t
.click(getNthDialogOptionsOption(3)) .click(getDialogOptionWithText('Block @baz'))
.expect(accountProfileFollowedBy.innerText).match(/blocked/i) .expect(accountProfileFollowedBy.innerText).match(/blocked/i)
.expect(accountProfileFollowButton.getAttribute('aria-label')).eql('Unblock') .expect(accountProfileFollowButton.getAttribute('aria-label')).eql('Unblock')
.expect(accountProfileFollowButton.getAttribute('title')).eql('Unblock') .expect(accountProfileFollowButton.getAttribute('title')).eql('Unblock')

View file

@ -5,11 +5,10 @@ import {
getNthSearchResult, getNthSearchResult,
getNthStatus, getNthStatus,
getNthStatusOptionsButton, getNthStatusOptionsButton,
getNthDialogOptionsOption,
getUrl, getUrl,
modalDialog, modalDialog,
closeDialogButton, closeDialogButton,
confirmationDialogOKButton, sleep confirmationDialogOKButton, sleep, getDialogOptionWithText
} from '../utils' } from '../utils'
import { Selector as $ } from 'testcafe' import { Selector as $ } from 'testcafe'
import { loginAsFoobar } from '../roles' import { loginAsFoobar } from '../roles'
@ -25,12 +24,9 @@ test('Can mute and unmute an account', async t => {
await t.expect(getNthStatus(1).innerText).contains(post, { timeout: 20000 }) await t.expect(getNthStatus(1).innerText).contains(post, { timeout: 20000 })
.click(getNthStatusOptionsButton(1)) .click(getNthStatusOptionsButton(1))
.expect(getNthDialogOptionsOption(1).innerText).contains('Unfollow @admin')
.expect(getNthDialogOptionsOption(2).innerText).contains('Block @admin')
.expect(getNthDialogOptionsOption(3).innerText).contains('Mute @admin')
await sleep(1000) await sleep(1000)
await t await t
.click(getNthDialogOptionsOption(3)) .click(getDialogOptionWithText('Mute @admin'))
await sleep(1000) await sleep(1000)
await t await t
.click(confirmationDialogOKButton) .click(confirmationDialogOKButton)
@ -43,20 +39,13 @@ test('Can mute and unmute an account', async t => {
.click(getNthSearchResult(1)) .click(getNthSearchResult(1))
.expect(getUrl()).contains('/accounts/1') .expect(getUrl()).contains('/accounts/1')
.click(accountProfileMoreOptionsButton) .click(accountProfileMoreOptionsButton)
.expect(getNthDialogOptionsOption(1).innerText).contains('Mention @admin')
.expect(getNthDialogOptionsOption(2).innerText).contains('Unfollow @admin')
.expect(getNthDialogOptionsOption(3).innerText).contains('Block @admin')
.expect(getNthDialogOptionsOption(4).innerText).contains('Unmute @admin')
await sleep(1000) await sleep(1000)
await t await t
.click(getNthDialogOptionsOption(4)) .click(getDialogOptionWithText('Unmute @admin'))
await sleep(1000) await sleep(1000)
await t await t
.click(accountProfileMoreOptionsButton) .click(accountProfileMoreOptionsButton)
.expect(getNthDialogOptionsOption(1).innerText).contains('Mention @admin') .expect(getDialogOptionWithText('Mute @admin').exists).ok()
.expect(getNthDialogOptionsOption(2).innerText).contains('Unfollow @admin')
.expect(getNthDialogOptionsOption(3).innerText).contains('Block @admin')
.expect(getNthDialogOptionsOption(4).innerText).contains('Mute @admin')
await sleep(1000) await sleep(1000)
await t await t
.click(closeDialogButton) .click(closeDialogButton)

View file

@ -0,0 +1,49 @@
import {
accountProfileMoreOptionsButton,
getNthStatus,
getNthStatusOptionsButton,
getUrl,
modalDialog,
sleep, getDialogOptionWithText, getNthStatusAccountLink, notificationsNavButton, getNthStatusHeader
} from '../utils'
import { loginAsFoobar } from '../roles'
import { postAs } from '../serverActions'
fixture`139-notify-denotify.js`
.page`http://localhost:4002`
test('Can notify and denotify an account', async t => {
await loginAsFoobar(t)
const post = 'ha ha ha'
await postAs('admin', post)
await t.expect(getNthStatus(1).innerText).contains(post, { timeout: 20000 })
.click(getNthStatusOptionsButton(1))
await sleep(1000)
await t.click(getDialogOptionWithText('Subscribe to @admin'))
await sleep(1000)
await t
.expect(modalDialog.exists).notOk()
await sleep(1000)
const notificationPost = 'get a notification for this'
await postAs('admin', notificationPost)
await sleep(1000)
await t
.expect(notificationsNavButton.getAttribute('aria-label')).eql('Notifications (1 notification)', {
timeout: 20000
})
.click(notificationsNavButton)
.expect(getUrl()).contains('/notifications')
await t
.expect(getNthStatus(1).innerText).contains(notificationPost, { timeout: 20000 })
.expect(getNthStatusHeader(1).innerText).contains('posted')
.click(getNthStatusAccountLink(1))
.expect(getUrl()).contains('/accounts/1')
.click(accountProfileMoreOptionsButton)
await sleep(1000)
await t.click(getDialogOptionWithText('Unsubscribe from @admin'))
await sleep(1000)
await t.click(accountProfileMoreOptionsButton)
await t
.expect(getDialogOptionWithText('Subscribe to @admin').exists).ok()
})

View file

@ -522,6 +522,10 @@ export function getNthStatusOptionsButton (n) {
return $(`${getNthStatusSelector(n)} .status-toolbar button:nth-child(4)`) return $(`${getNthStatusSelector(n)} .status-toolbar button:nth-child(4)`)
} }
export function getNthStatusAccountLink (n) {
return $(`${getNthStatusSelector(n)} .status-author-name`)
}
export function getNthFavoritedLabel (n) { export function getNthFavoritedLabel (n) {
return getNthFavoriteButton(n).getAttribute('aria-label') return getNthFavoriteButton(n).getAttribute('aria-label')
} }
@ -546,6 +550,10 @@ export function getNthDialogOptionsOption (n) {
return $(`.modal-dialog li:nth-child(${n}) button`) return $(`.modal-dialog li:nth-child(${n}) button`)
} }
export function getDialogOptionWithText (text) {
return $('.modal-dialog li button').withText(text)
}
export function getReblogsCount () { export function getReblogsCount () {
return reblogsCountElement.innerCount return reblogsCountElement.innerCount
} }