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 { emit } from '../_utils/eventBus'
export async function setAccountMuted (accountId, mute, toastOnSuccess) {
export async function setAccountMuted (accountId, mute, notifications, toastOnSuccess) {
let { currentInstance, accessToken } = store.get()
try {
let relationship
if (mute) {
relationship = await muteAccount(currentInstance, accessToken, accountId)
relationship = await muteAccount(currentInstance, accessToken, accountId, notifications)
} else {
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 { 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`
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) {

View File

@ -8,8 +8,8 @@ export const importShowComposeDialog = () => import(
/* webpackChunkName: 'showComposeDialog' */ './creators/showComposeDialog'
).then(getDefault)
export const importShowConfirmationDialog = () => import(
/* webpackChunkName: 'showConfirmationDialog' */ './creators/showConfirmationDialog'
export const importShowTextConfirmationDialog = () => import(
/* webpackChunkName: 'showTextConfirmationDialog' */ './creators/showTextConfirmationDialog'
).then(getDefault)
export const importShowEmojiDialog = () => import(
@ -35,3 +35,7 @@ export const importShowShortcutHelpDialog = () => import(
export const importShowMediaDialog = () => import(
/* webpackChunkName: 'showMediaDialog' */ './creators/showMediaDialog'
).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 { oncreate } from '../helpers/onCreateDialog'
import { setAccountBlocked } from '../../../_actions/block'
import { setAccountMuted } from '../../../_actions/mute'
import { setAccountFollowed } from '../../../_actions/follow'
import { setShowReblogs } from '../../../_actions/setShowReblogs'
import { setDomainBlocked } from '../../../_actions/setDomainBlocked'
import { copyText } from '../../../_actions/copyText'
import { composeNewStatusMentioning } from '../../../_actions/mention'
import { toggleMute } from '../../../_actions/toggleMute'
export default {
oncreate,
@ -155,9 +155,9 @@ export default {
await setAccountBlocked(accountId, !blocking, true)
},
async onMuteClicked () {
let { accountId, muting } = this.get()
let { account, muting } = this.get()
this.close()
await setAccountMuted(accountId, !muting, true)
await toggleMute(account, !muting)
},
async onShowReblogsClicked () {
let { accountId, showingReblogs } = this.get()

View File

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

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 { oncreate } from '../helpers/onCreateDialog'
import { setAccountBlocked } from '../../../_actions/block'
import { setAccountMuted } from '../../../_actions/mute'
import { setStatusPinnedOrUnpinned } from '../../../_actions/pin'
import { setConversationMuted } from '../../../_actions/muteConversation'
import { copyText } from '../../../_actions/copyText'
import { deleteAndRedraft } from '../../../_actions/deleteAndRedraft'
import { shareStatus } from '../../../_actions/share'
import { toggleMute } from '../../../_actions/toggleMute'
export default {
oncreate,
@ -183,9 +183,9 @@ export default {
await setAccountBlocked(accountId, !blocking, true)
},
async onMuteClicked () {
let { accountId, muting } = this.get()
let { account, muting } = this.get()
this.close()
await setAccountMuted(accountId, !muting, true)
await toggleMute(account, !muting)
},
async onMuteConversationClicked () {
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 { createDialogId } from '../helpers/createDialogId'
export default function showConfirmationDialog (options) {
let dialog = new ConfirmationDialog({
export default function showTextConfirmationDialog (options) {
let dialog = new TextConfirmationDialog({
target: createDialogElement(),
data: Object.assign({
id: createDialogId(),
@ -11,4 +11,5 @@ export default function showConfirmationDialog (options) {
}, options)
})
dialog.show()
return dialog
}

View File

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

View File

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

View File

@ -15,7 +15,10 @@
{
icon: '#fa-volume-up',
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 {
addInstanceButton,
authorizeInput,
authorizeInput, confirmationDialogOKButton,
emailInput,
formError,
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-display-name').innerText).eql('foobar')
.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")
.click(homeNavButton)
// 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-display-name').innerText).eql('foobar')
.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")
.click(homeNavButton)
.expect(getOpacity('.hidden-from-ssr')()).eql('1')

View File

@ -1,7 +1,15 @@
import {
accountProfileFollowButton,
accountProfileMoreOptionsButton, communityNavButton, getNthSearchResult,
getNthStatus, getNthStatusOptionsButton, getNthDialogOptionsOption, getUrl, modalDialog, closeDialogButton
accountProfileMoreOptionsButton,
communityNavButton,
getNthSearchResult,
getNthStatus,
getNthStatusOptionsButton,
getNthDialogOptionsOption,
getUrl,
modalDialog,
closeDialogButton,
confirmationDialogOKButton, sleep
} from '../utils'
import { Selector as $ } from 'testcafe'
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(3).innerText).contains('Mute @admin')
.click(getNthDialogOptionsOption(3))
await sleep(1000)
await t
.click(confirmationDialogOKButton)
.expect(modalDialog.exists).notOk()
await sleep(1000)
await t
.click(communityNavButton)
.click($('a[href="/muted"]'))
.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(4).innerText).contains('Unmute @admin')
.click(getNthDialogOptionsOption(4))
await sleep(1000)
await t
.click(accountProfileMoreOptionsButton)
.expect(getNthDialogOptionsOption(1).innerText).contains('Mention @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 dialogOptionsOption = $(`.modal-dialog button`)
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 composeModalComposeButton = $('.modal-dialog .compose-box-button')