Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

Allow to reuse sounds for custom notifications #6002

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
182 changes: 125 additions & 57 deletions src/components/views/settings/tabs/room/NotificationSettingsTab.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,12 @@ import PropTypes from 'prop-types';
import {_t} from "../../../../../languageHandler";
import {MatrixClientPeg} from "../../../../../MatrixClientPeg";
import AccessibleButton from "../../../elements/AccessibleButton";
import Notifier from "../../../../../Notifier";
import SettingsStore from '../../../../../settings/SettingsStore';
import {SettingLevel} from "../../../../../settings/SettingLevel";
import {replaceableComponent} from "../../../../../utils/replaceableComponent";
import Field from "../../../elements/Field";
import Modal from '../../../../../Modal';
import * as sdk from '../../../../../index';

@replaceableComponent("views.settings.tabs.room.NotificationsSettingsTab")
export default class NotificationsSettingsTab extends React.Component {
Expand All @@ -37,37 +39,91 @@ export default class NotificationsSettingsTab extends React.Component {

this.state = {
currentSound: "default",
uploadedFile: null,
currentSoundReplaced: false,
selected: null,
soundLibrary: {},
};
}

// TODO: [REACT-WARNING] Replace component with real class, use constructor for refs
UNSAFE_componentWillMount() { // eslint-disable-line camelcase
const soundData = Notifier.getSoundForRoom(this.props.roomId);
if (!soundData) {
return;
}
this.setState({currentSound: soundData.name || soundData.url});
const soundData = SettingsStore.getValue("notificationSound", this.props.roomId);
const soundLibrary = SettingsStore.getValue("soundLibrary", null);
console.log(soundLibrary);
const selected = (soundData === null) ? "default" : soundData.name;
this.setState({
currentSound: soundData.name || soundData.url,
selected: selected,
soundLibrary: soundLibrary,
});
}

async _triggerUploader(e) {
e.stopPropagation();
e.preventDefault();

async _triggerUploader() {
this._soundUpload.current.click();
}

async _onSoundUploadChanged(e) {
if (!e.target.files || !e.target.files.length) {
this.setState({
uploadedFile: null,
});
return;
}

const file = e.target.files[0];
const soundLibrary = this.state.soundLibrary;

if (file.name in soundLibrary) {
const QuestionDialog = sdk.getComponent('dialogs.QuestionDialog');
Modal.createDialog(QuestionDialog, {
title: _t("Replace File"),
description: _t("There already exists a file with this name. " +
"Are you sure, you want to replace it?"),
button: _t("Replace"),
onFinished: () => {
this.setState({currentSoundReplaced: true});
this._uploadSound(file);
},
});

return;
}
this._uploadSound(file);
}

async _uploadSound(file) {
let type = file.type;

if (type === "video/ogg") {
// XXX: I've observed browsers allowing users to pick a audio/ogg files,
// and then calling it a video/ogg. This is a lame hack, but man browsers
// suck at detecting mimetypes.
type = "audio/ogg";
}

const url = await MatrixClientPeg.get().uploadContent(
file, {
type,
},
);

const soundJSON = {
name: file.name,
type: type,
size: file.size,
url,
};

const soundLibrary = this.state.soundLibrary;
soundLibrary[soundJSON.name] = soundJSON;

await SettingsStore.setValue(
"soundLibrary",
null,
SettingLevel.ACCOUNT,
soundLibrary,
);

this.setState({
uploadedFile: file,
soundLibrary: soundLibrary,
selected: soundJSON.name,
});
}

Expand All @@ -86,45 +142,30 @@ export default class NotificationsSettingsTab extends React.Component {
}

async _saveSound() {
if (!this.state.uploadedFile) {
if (!this.state.selected) {
return;
}

let type = this.state.uploadedFile.type;
if (type === "video/ogg") {
// XXX: I've observed browsers allowing users to pick a audio/ogg files,
// and then calling it a video/ogg. This is a lame hack, but man browsers
// suck at detecting mimetypes.
type = "audio/ogg";
if (this.state.selected == "default") {
this._clearSound();
return;
}

const url = await MatrixClientPeg.get().uploadContent(
this.state.uploadedFile, {
type,
},
);
const soundJSON = this.state.soundLibrary[this.state.selected];

await SettingsStore.setValue(
"notificationSound",
this.props.roomId,
SettingLevel.ROOM_ACCOUNT,
{
name: this.state.uploadedFile.name,
type: type,
size: this.state.uploadedFile.size,
url,
},
soundJSON,
);

this.setState({
uploadedFile: null,
currentSound: this.state.uploadedFile.name,
currentSound: soundJSON.name,
});
}

_clearSound(e) {
e.stopPropagation();
e.preventDefault();
_clearSound() {
SettingsStore.setValue(
"notificationSound",
this.props.roomId,
Expand All @@ -137,40 +178,67 @@ export default class NotificationsSettingsTab extends React.Component {
});
}

render() {
let currentUploadedFile = null;
if (this.state.uploadedFile) {
currentUploadedFile = (
<div>
<span>{_t("Uploaded sound")}: <code>{this.state.uploadedFile.name}</code></span>
</div>
);
_onReset() {
this.setState({
selected: this.state.currentSound,
});
}

_onChangeSelection(e) {
e.stopPropagation();
e.preventDefault();

if (e.target.value === "upload") {
this._triggerUploader();
return;
}

this.setState({
selected: e.target.value,
});
}


render() {
const notChanged = this.state.currentSound == this.state.selected && !this.state.currentSoundReplaced;
const soundOptions = Object.keys(this.state.soundLibrary)
.map((sound, i) => <option key={i} value={sound}>{sound}</option>);

return (
<div className="mx_SettingsTab">
<div className="mx_SettingsTab_heading">{_t("Notifications")}</div>
<div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'>
<span className='mx_SettingsTab_subheading'>{_t("Sounds")}</span>
<div>
<span>{_t("Notification sound")}: <code>{this.state.currentSound}</code></span><br />
<AccessibleButton className="mx_NotificationSound_resetSound" disabled={this.state.currentSound == "default"} onClick={this._clearSound.bind(this)} kind="primary">
{_t("Reset")}
</AccessibleButton>
</div>
<div>
<h3>{_t("Set a new custom sound")}</h3>
<h3>{_t("Select a custom sound")}</h3>
<Field
id="soundLibrary"
element="select"
label="custom sound"
value={this.state.selected}
onChange={this._onChangeSelection.bind(this)}
>
<option key="default" value="default">{_t("Default")}</option>
{soundOptions}
<option key="uplod" value="upload">{_t("upload")}</option>
</Field>
<form autoComplete="off" noValidate={true}>
<input ref={this._soundUpload} className="mx_NotificationSound_soundUpload" type="file" onChange={this._onSoundUploadChanged.bind(this)} accept="audio/*" />
<input
ref={this._soundUpload}
className="mx_NotificationSound_soundUpload"
type="file"
onChange={this._onSoundUploadChanged.bind(this)}
accept="audio/*"
/>
</form>

{currentUploadedFile}

<AccessibleButton className="mx_NotificationSound_browse" onClick={this._triggerUploader.bind(this)} kind="primary">
{_t("Browse")}
<AccessibleButton className="mx_NotificationSound_resetSound" disabled={notChanged} onClick={this._onReset.bind(this)} kind="primary">
{_t("Reset")}
</AccessibleButton>

<AccessibleButton className="mx_NotificationSound_save" disabled={this.state.uploadedFile == null} onClick={this._onClickSaveSound.bind(this)} kind="primary">
<AccessibleButton className="mx_NotificationSound_save" disabled={notChanged} onClick={this._onClickSaveSound.bind(this)} kind="primary">
{_t("Save")}
</AccessibleButton>
<br />
Expand Down
8 changes: 5 additions & 3 deletions src/i18n/strings/en_EN.json
Original file line number Diff line number Diff line change
Expand Up @@ -1352,11 +1352,13 @@
"Bridges": "Bridges",
"URL Previews": "URL Previews",
"Room Addresses": "Room Addresses",
"Uploaded sound": "Uploaded sound",
"Replace File": "Replace File",
"There already exists a file with this name. Are you sure, you want to replace it?": "There already exists a file with this name. Are you sure, you want to replace it?",
"Replace": "Replace",
"Sounds": "Sounds",
"Notification sound": "Notification sound",
"Set a new custom sound": "Set a new custom sound",
"Browse": "Browse",
"Select a custom sound": "Select a custom sound",
"upload": "upload",
"Change room avatar": "Change room avatar",
"Change room name": "Change room name",
"Change main address for the room": "Change main address for the room",
Expand Down
4 changes: 4 additions & 0 deletions src/settings/Settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -561,6 +561,10 @@ export const SETTINGS: {[setting: string]: ISetting} = {
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS,
default: true,
},
"soundLibrary": {
supportedLevels: [SettingLevel.ACCOUNT],
default: {},
},
"enableWidgetScreenshots": {
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
displayName: _td('Enable widget screenshots on supported widgets'),
Expand Down