feat: allow muting notifications when muting (#1013)

fixes #738
This commit is contained in:
Nolan Lawson 2019-02-18 15:43:41 -08:00 committed by GitHub
parent ebbe6ba9f8
commit 7a152fbdac
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 276 additions and 41 deletions

View file

@ -4,12 +4,12 @@ import { toast } from '../_components/toast/toast'
import { updateLocalRelationship } from './accounts' import { updateLocalRelationship } from './accounts'
import { emit } from '../_utils/eventBus' import { emit } from '../_utils/eventBus'
export async function setAccountMuted (accountId, mute, toastOnSuccess) { export async function setAccountMuted (accountId, mute, notifications, toastOnSuccess) {
let { currentInstance, accessToken } = store.get() let { currentInstance, accessToken } = store.get()
try { try {
let relationship let relationship
if (mute) { if (mute) {
relationship = await muteAccount(currentInstance, accessToken, accountId) relationship = await muteAccount(currentInstance, accessToken, accountId, notifications)
} else { } else {
relationship = await unmuteAccount(currentInstance, accessToken, accountId) relationship = await unmuteAccount(currentInstance, accessToken, accountId)
} }

View file

@ -0,0 +1,10 @@
import { importShowMuteDialog } from '../_components/dialog/asyncDialogs'
import { setAccountMuted } from './mute'
export async function toggleMute (account, mute) {
if (mute) {
(await importShowMuteDialog())(account)
} else {
await setAccountMuted(account.id, mute, /* notifications */ false, /* toastOnSuccess */ true)
}
}

View file

@ -1,9 +1,9 @@
import { auth, basename } from './utils' import { auth, basename } from './utils'
import { post, WRITE_TIMEOUT } from '../_utils/ajax' import { post, WRITE_TIMEOUT } from '../_utils/ajax'
export async function muteAccount (instanceName, accessToken, accountId) { export async function muteAccount (instanceName, accessToken, accountId, notifications) {
let url = `${basename(instanceName)}/api/v1/accounts/${accountId}/mute` let url = `${basename(instanceName)}/api/v1/accounts/${accountId}/mute`
return post(url, null, auth(accessToken), { timeout: WRITE_TIMEOUT }) return post(url, { notifications }, auth(accessToken), { timeout: WRITE_TIMEOUT })
} }
export async function unmuteAccount (instanceName, accessToken, accountId) { export async function unmuteAccount (instanceName, accessToken, accountId) {

View file

@ -8,8 +8,8 @@ export const importShowComposeDialog = () => import(
/* webpackChunkName: 'showComposeDialog' */ './creators/showComposeDialog' /* webpackChunkName: 'showComposeDialog' */ './creators/showComposeDialog'
).then(getDefault) ).then(getDefault)
export const importShowConfirmationDialog = () => import( export const importShowTextConfirmationDialog = () => import(
/* webpackChunkName: 'showConfirmationDialog' */ './creators/showConfirmationDialog' /* webpackChunkName: 'showTextConfirmationDialog' */ './creators/showTextConfirmationDialog'
).then(getDefault) ).then(getDefault)
export const importShowEmojiDialog = () => import( export const importShowEmojiDialog = () => import(
@ -35,3 +35,7 @@ export const importShowShortcutHelpDialog = () => import(
export const importShowMediaDialog = () => import( export const importShowMediaDialog = () => import(
/* webpackChunkName: 'showMediaDialog' */ './creators/showMediaDialog' /* webpackChunkName: 'showMediaDialog' */ './creators/showMediaDialog'
).then(getDefault) ).then(getDefault)
export const importShowMuteDialog = () => import(
/* webpackChunkName: 'showMuteDialog' */ './creators/showMuteDialog'
).then(getDefault)

View file

@ -15,12 +15,12 @@ import { show } from '../helpers/showDialog'
import { close } from '../helpers/closeDialog' import { close } from '../helpers/closeDialog'
import { oncreate } from '../helpers/onCreateDialog' import { oncreate } from '../helpers/onCreateDialog'
import { setAccountBlocked } from '../../../_actions/block' import { setAccountBlocked } from '../../../_actions/block'
import { setAccountMuted } from '../../../_actions/mute'
import { setAccountFollowed } from '../../../_actions/follow' import { setAccountFollowed } from '../../../_actions/follow'
import { setShowReblogs } from '../../../_actions/setShowReblogs' import { setShowReblogs } from '../../../_actions/setShowReblogs'
import { setDomainBlocked } from '../../../_actions/setDomainBlocked' import { setDomainBlocked } from '../../../_actions/setDomainBlocked'
import { copyText } from '../../../_actions/copyText' import { copyText } from '../../../_actions/copyText'
import { composeNewStatusMentioning } from '../../../_actions/mention' import { composeNewStatusMentioning } from '../../../_actions/mention'
import { toggleMute } from '../../../_actions/toggleMute'
export default { export default {
oncreate, oncreate,
@ -155,9 +155,9 @@ export default {
await setAccountBlocked(accountId, !blocking, true) await setAccountBlocked(accountId, !blocking, true)
}, },
async onMuteClicked () { async onMuteClicked () {
let { accountId, muting } = this.get() let { account, muting } = this.get()
this.close() this.close()
await setAccountMuted(accountId, !muting, true) await toggleMute(account, !muting)
}, },
async onShowReblogsClicked () { async onShowReblogsClicked () {
let { accountId, showingReblogs } = this.get() let { accountId, showingReblogs } = this.get()

View file

@ -1,18 +1,21 @@
<ModalDialog <ModalDialog
{id} {id}
{label} {label}
{title}
background="var(--main-bg)" background="var(--main-bg)"
> >
<form class="confirmation-dialog-form"> <form class="confirmation-dialog-form">
<p> {#if component}
{text} <svelte:component this={component} {...componentOpts} />
</p> {:else}
<p>{text}</p>
{/if}
<div class="confirmation-dialog-form-flex"> <div class="confirmation-dialog-form-flex">
<button type="button" on:click="onPositive()"> <button type="button" on:click="onPositive()">
{positiveText || 'OK'} {positiveText}
</button> </button>
<button type="button" on:click="onNegative()"> <button type="button" on:click="onNegative()">
{negativeText || 'Cancel'} {negativeText}
</button> </button>
</div> </div>
</form> </form>
@ -44,6 +47,15 @@
on('destroyDialog', this, this.onDestroyDialog) on('destroyDialog', this, this.onDestroyDialog)
onCreateDialog.call(this) onCreateDialog.call(this)
}, },
data: () => ({
component: void 0,
text: void 0,
onPositive: void 0,
onNegative: void 0,
title: '',
positiveText: 'OK',
negativeText: 'Cancel'
}),
methods: { methods: {
show, show,
close, close,
@ -58,10 +70,12 @@
return return
} }
if (positiveResult) { if (positiveResult) {
this.fire('positive')
if (onPositive) { if (onPositive) {
onPositive() onPositive()
} }
} else { } else {
this.fire('negative')
if (onNegative) { if (onNegative) {
onNegative() onNegative()
} }

View file

@ -0,0 +1,75 @@
<ModalDialog
{id}
{label}
{title}
background="var(--main-bg)"
>
<form class="confirmation-dialog-form">
<slot></slot>
<div class="confirmation-dialog-form-flex">
<button type="button" on:click="onPositive()">
{positiveText || 'OK'}
</button>
<button type="button" on:click="onNegative()">
{negativeText || 'Cancel'}
</button>
</div>
</form>
</ModalDialog>
<style>
.confirmation-dialog-form-flex {
display: grid;
grid-template-columns: 1fr 1fr;
grid-gap: 10px;
padding: 10px 20px;
}
.confirmation-dialog-form-flex button {
min-width: 125px;
}
</style>
<script>
import ModalDialog from './ModalDialog.html'
import { show } from '../helpers/showDialog'
import { close } from '../helpers/closeDialog'
import { on } from '../../../_utils/eventBus'
import { oncreate as onCreateDialog } from '../helpers/onCreateDialog'
export default {
oncreate () {
on('destroyDialog', this, this.onDestroyDialog)
onCreateDialog.call(this)
},
data: () => ({
positiveText: void 0,
negativeText: void 0
}),
methods: {
show,
close,
onDestroyDialog (thisId) {
let {
id,
positiveResult
} = this.get()
if (thisId !== id) {
return
}
if (positiveResult) {
this.fire('positive')
} else {
this.fire('negative')
}
},
onPositive () {
this.set({ positiveResult: true })
this.close()
},
onNegative () {
this.close()
}
},
components: {
ModalDialog
}
}
</script>

View file

@ -0,0 +1,60 @@
<GenericConfirmationDialog
{id}
{label}
{title}
{positiveText}
on:positive="doMute()"
>
<div class="mute-dialog">
<p>
Mute @{account.acct} ?
</p>
<form class="mute-dialog-form">
<input type="checkbox"
id="mute-notifications"
name="mute-notifications"
bind:checked="muteNotifications">
<label for="mute-notifications">Mute notifications as well</label>
</form>
</div>
</GenericConfirmationDialog>
<style>
.mute-dialog {
padding: 20px;
}
.mute-dialog-form {
margin-top: 20px;
}
</style>
<script>
import GenericConfirmationDialog from './GenericConfirmationDialog.html'
import { show } from '../helpers/showDialog'
import { close } from '../helpers/closeDialog'
import { oncreate } from '../helpers/onCreateDialog'
import { setAccountMuted } from '../../../_actions/mute'
export default {
oncreate,
data: () => ({
positiveText: 'Mute',
title: '',
muteNotifications: true
}),
methods: {
show,
close,
async doMute () {
let { account, muteNotifications } = this.get()
this.close()
await setAccountMuted(
account.id,
/* mute */ true,
muteNotifications,
/* toastOnSuccess */ true)
}
},
components: {
GenericConfirmationDialog
}
}
</script>

View file

@ -16,12 +16,12 @@ import { show } from '../helpers/showDialog'
import { close } from '../helpers/closeDialog' import { close } from '../helpers/closeDialog'
import { oncreate } from '../helpers/onCreateDialog' import { oncreate } from '../helpers/onCreateDialog'
import { setAccountBlocked } from '../../../_actions/block' import { setAccountBlocked } from '../../../_actions/block'
import { setAccountMuted } from '../../../_actions/mute'
import { setStatusPinnedOrUnpinned } from '../../../_actions/pin' import { setStatusPinnedOrUnpinned } from '../../../_actions/pin'
import { setConversationMuted } from '../../../_actions/muteConversation' import { setConversationMuted } from '../../../_actions/muteConversation'
import { copyText } from '../../../_actions/copyText' import { copyText } from '../../../_actions/copyText'
import { deleteAndRedraft } from '../../../_actions/deleteAndRedraft' import { deleteAndRedraft } from '../../../_actions/deleteAndRedraft'
import { shareStatus } from '../../../_actions/share' import { shareStatus } from '../../../_actions/share'
import { toggleMute } from '../../../_actions/toggleMute'
export default { export default {
oncreate, oncreate,
@ -183,9 +183,9 @@ export default {
await setAccountBlocked(accountId, !blocking, true) await setAccountBlocked(accountId, !blocking, true)
}, },
async onMuteClicked () { async onMuteClicked () {
let { accountId, muting } = this.get() let { account, muting } = this.get()
this.close() this.close()
await setAccountMuted(accountId, !muting, true) await toggleMute(account, !muting)
}, },
async onMuteConversationClicked () { async onMuteConversationClicked () {
let { statusId, mutingConversation } = this.get() let { statusId, mutingConversation } = this.get()

View file

@ -0,0 +1,38 @@
<GenericConfirmationDialog
{id}
{label}
{title}
{positiveText}
{negativeText}
on:positive
on:negative>
<p>{text}</p>
</GenericConfirmationDialog>
<style>
p {
font-size: 1.3em;
padding: 40px 20px;
}
</style>
<script>
import GenericConfirmationDialog from './GenericConfirmationDialog.html'
import { show } from '../helpers/showDialog'
import { close } from '../helpers/closeDialog'
import { oncreate } from '../helpers/onCreateDialog'
export default {
oncreate,
data: () => ({
title: void 0,
positiveText: void 0,
negativeText: void 0
}),
methods: {
show,
close
},
components: {
GenericConfirmationDialog
}
}
</script>

View file

@ -0,0 +1,16 @@
import MuteDialog from '../components/MuteDialog.html'
import { createDialogElement } from '../helpers/createDialogElement'
import { createDialogId } from '../helpers/createDialogId'
export default function showMuteDialog (account) {
let dialog = new MuteDialog({
target: createDialogElement(),
data: {
id: createDialogId(),
label: 'Mute dialog',
account
}
})
dialog.show()
return dialog
}

View file

@ -1,9 +1,9 @@
import ConfirmationDialog from '../components/ConfirmationDialog.html' import TextConfirmationDialog from '../components/TextConfirmationDialog.html'
import { createDialogElement } from '../helpers/createDialogElement' import { createDialogElement } from '../helpers/createDialogElement'
import { createDialogId } from '../helpers/createDialogId' import { createDialogId } from '../helpers/createDialogId'
export default function showConfirmationDialog (options) { export default function showTextConfirmationDialog (options) {
let dialog = new ConfirmationDialog({ let dialog = new TextConfirmationDialog({
target: createDialogElement(), target: createDialogElement(),
data: Object.assign({ data: Object.assign({
id: createDialogId(), id: createDialogId(),
@ -11,4 +11,5 @@ export default function showConfirmationDialog (options) {
}, options) }, options)
}) })
dialog.show() dialog.show()
return dialog
} }

View file

@ -21,7 +21,7 @@
</style> </style>
<script> <script>
import { store } from '../../../_store/store' import { store } from '../../../_store/store'
import { importShowConfirmationDialog } from '../../dialog/asyncDialogs' import { importShowTextConfirmationDialog } from '../../dialog/asyncDialogs'
import { switchToInstance, logOutOfInstance } from '../../../_actions/instances' import { switchToInstance, logOutOfInstance } from '../../../_actions/instances'
export default { export default {
@ -36,12 +36,11 @@
e.preventDefault() e.preventDefault()
let { instanceName } = this.get() let { instanceName } = this.get()
let showConfirmationDialog = await importShowConfirmationDialog() let showTextConfirmationDialog = await importShowTextConfirmationDialog()
showConfirmationDialog({ showTextConfirmationDialog({
text: `Log out of ${instanceName}?`, text: `Log out of ${instanceName}?`
onPositive () { }).on('positive', () => {
/* no await */ logOutOfInstance(instanceName) /* no await */ logOutOfInstance(instanceName)
}
}) })
} }
} }

View file

@ -36,7 +36,7 @@
</style> </style>
<script> <script>
import { store } from '../../../_store/store' import { store } from '../../../_store/store'
import { importShowConfirmationDialog } from '../../dialog/asyncDialogs' import { importShowTextConfirmationDialog } from '../../dialog/asyncDialogs'
import { logOutOfInstance } from '../../../_actions/instances' import { logOutOfInstance } from '../../../_actions/instances'
import { updatePushSubscriptionForInstance, updateAlerts } from '../../../_actions/pushSubscription' import { updatePushSubscriptionForInstance, updateAlerts } from '../../../_actions/pushSubscription'
import { toast } from '../../toast/toast' import { toast } from '../../toast/toast'
@ -76,12 +76,11 @@
// TODO: Better way to detect missing authorization scope // TODO: Better way to detect missing authorization scope
if (err.message.startsWith('403:')) { if (err.message.startsWith('403:')) {
let showConfirmationDialog = await importShowConfirmationDialog() let showTextConfirmationDialog = await importShowTextConfirmationDialog()
showConfirmationDialog({ showTextConfirmationDialog({
text: `You need to reauthenticate in order to enable push notification. Log out of ${instanceName}?`, text: `You need to reauthenticate in order to enable push notification. Log out of ${instanceName}?`
onPositive () { }).on('positive', () => {
logOutOfInstance(instanceName) /* no await */ logOutOfInstance(instanceName)
}
}) })
} else { } else {
toast.say(`Failed to update push notification settings: ${err.message}`) toast.say(`Failed to update push notification settings: ${err.message}`)

View file

@ -15,7 +15,10 @@
{ {
icon: '#fa-volume-up', icon: '#fa-volume-up',
label: 'Unmute', label: 'Unmute',
onclick: (accountId) => setAccountMuted(accountId, false, true) onclick: (accountId) => setAccountMuted(accountId,
/* mute */ false,
/* notifications */ false,
/* toastOnSuccess */ true)
} }
] ]
}), }),

View file

@ -1,7 +1,7 @@
import { Selector as $ } from 'testcafe' import { Selector as $ } from 'testcafe'
import { import {
addInstanceButton, addInstanceButton,
authorizeInput, authorizeInput, confirmationDialogOKButton,
emailInput, emailInput,
formError, formError,
getFirstVisibleStatus, getNthStatus, getOpacity, getFirstVisibleStatus, getNthStatus, getOpacity,
@ -62,7 +62,7 @@ test('Logs in and logs out of localhost:3000', async t => {
.expect($('.acct-handle').innerText).eql('@foobar') .expect($('.acct-handle').innerText).eql('@foobar')
.expect($('.acct-display-name').innerText).eql('foobar') .expect($('.acct-display-name').innerText).eql('foobar')
.click($('button').withText('Log out')) .click($('button').withText('Log out'))
.click($('.modal-dialog button').withText('OK')) .click(confirmationDialogOKButton)
.expect($('.main-content').innerText).contains("You're not logged in to any instances") .expect($('.main-content').innerText).contains("You're not logged in to any instances")
.click(homeNavButton) .click(homeNavButton)
// check that the "hidden from SSR" content is visible // check that the "hidden from SSR" content is visible
@ -89,7 +89,7 @@ test('Logs in, refreshes, then logs out', async t => {
.expect($('.acct-handle').innerText).eql('@foobar') .expect($('.acct-handle').innerText).eql('@foobar')
.expect($('.acct-display-name').innerText).eql('foobar') .expect($('.acct-display-name').innerText).eql('foobar')
.click($('button').withText('Log out')) .click($('button').withText('Log out'))
.click($('.modal-dialog button').withText('OK')) .click(confirmationDialogOKButton)
.expect($('.main-content').innerText).contains("You're not logged in to any instances") .expect($('.main-content').innerText).contains("You're not logged in to any instances")
.click(homeNavButton) .click(homeNavButton)
.expect(getOpacity('.hidden-from-ssr')()).eql('1') .expect(getOpacity('.hidden-from-ssr')()).eql('1')

View file

@ -1,7 +1,15 @@
import { import {
accountProfileFollowButton, accountProfileFollowButton,
accountProfileMoreOptionsButton, communityNavButton, getNthSearchResult, accountProfileMoreOptionsButton,
getNthStatus, getNthStatusOptionsButton, getNthDialogOptionsOption, getUrl, modalDialog, closeDialogButton communityNavButton,
getNthSearchResult,
getNthStatus,
getNthStatusOptionsButton,
getNthDialogOptionsOption,
getUrl,
modalDialog,
closeDialogButton,
confirmationDialogOKButton, sleep
} from '../utils' } from '../utils'
import { Selector as $ } from 'testcafe' import { Selector as $ } from 'testcafe'
import { loginAsFoobar } from '../roles' import { loginAsFoobar } from '../roles'
@ -21,7 +29,12 @@ test('Can mute and unmute an account', async t => {
.expect(getNthDialogOptionsOption(2).innerText).contains('Block @admin') .expect(getNthDialogOptionsOption(2).innerText).contains('Block @admin')
.expect(getNthDialogOptionsOption(3).innerText).contains('Mute @admin') .expect(getNthDialogOptionsOption(3).innerText).contains('Mute @admin')
.click(getNthDialogOptionsOption(3)) .click(getNthDialogOptionsOption(3))
await sleep(1000)
await t
.click(confirmationDialogOKButton)
.expect(modalDialog.exists).notOk() .expect(modalDialog.exists).notOk()
await sleep(1000)
await t
.click(communityNavButton) .click(communityNavButton)
.click($('a[href="/muted"]')) .click($('a[href="/muted"]'))
.expect(getNthSearchResult(1).innerText).contains('@admin') .expect(getNthSearchResult(1).innerText).contains('@admin')
@ -33,6 +46,8 @@ test('Can mute and unmute an account', async t => {
.expect(getNthDialogOptionsOption(3).innerText).contains('Block @admin') .expect(getNthDialogOptionsOption(3).innerText).contains('Block @admin')
.expect(getNthDialogOptionsOption(4).innerText).contains('Unmute @admin') .expect(getNthDialogOptionsOption(4).innerText).contains('Unmute @admin')
.click(getNthDialogOptionsOption(4)) .click(getNthDialogOptionsOption(4))
await sleep(1000)
await t
.click(accountProfileMoreOptionsButton) .click(accountProfileMoreOptionsButton)
.expect(getNthDialogOptionsOption(1).innerText).contains('Mention @admin') .expect(getNthDialogOptionsOption(1).innerText).contains('Mention @admin')
.expect(getNthDialogOptionsOption(2).innerText).contains('Unfollow @admin') .expect(getNthDialogOptionsOption(2).innerText).contains('Unfollow @admin')

View file

@ -47,6 +47,7 @@ export const neverMarkMediaSensitiveInput = $('#choice-never-mark-media-sensitiv
export const removeEmojiFromDisplayNamesInput = $('#choice-omit-emoji-in-display-names') export const removeEmojiFromDisplayNamesInput = $('#choice-omit-emoji-in-display-names')
export const dialogOptionsOption = $(`.modal-dialog button`) export const dialogOptionsOption = $(`.modal-dialog button`)
export const emojiSearchInput = $('.emoji-mart-search input') export const emojiSearchInput = $('.emoji-mart-search input')
export const confirmationDialogOKButton = $('.confirmation-dialog-form-flex button:nth-child(1)')
export const composeModalInput = $('.modal-dialog .compose-box-input') export const composeModalInput = $('.modal-dialog .compose-box-input')
export const composeModalComposeButton = $('.modal-dialog .compose-box-button') export const composeModalComposeButton = $('.modal-dialog .compose-box-button')