Merge remote-tracking branch 'origin/develop' into develop
This commit is contained in:
commit
c1d70f28d7
5 changed files with 77 additions and 50 deletions
11
src/Roles.js
11
src/Roles.js
|
@ -15,19 +15,20 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
import { _t } from './languageHandler';
|
import { _t } from './languageHandler';
|
||||||
|
|
||||||
export function levelRoleMap() {
|
export function levelRoleMap(usersDefault) {
|
||||||
return {
|
return {
|
||||||
undefined: _t('Default'),
|
undefined: _t('Default'),
|
||||||
0: _t('User'),
|
0: _t('Restricted'),
|
||||||
|
[usersDefault]: _t('Default'),
|
||||||
50: _t('Moderator'),
|
50: _t('Moderator'),
|
||||||
100: _t('Admin'),
|
100: _t('Admin'),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function textualPowerLevel(level, userDefault) {
|
export function textualPowerLevel(level, usersDefault) {
|
||||||
const LEVEL_ROLE_MAP = this.levelRoleMap();
|
const LEVEL_ROLE_MAP = this.levelRoleMap(usersDefault);
|
||||||
if (LEVEL_ROLE_MAP[level]) {
|
if (LEVEL_ROLE_MAP[level]) {
|
||||||
return LEVEL_ROLE_MAP[level] + (level !== undefined ? ` (${level})` : ` (${userDefault})`);
|
return LEVEL_ROLE_MAP[level] + (level !== undefined ? ` (${level})` : ` (${usersDefault})`);
|
||||||
} else {
|
} else {
|
||||||
return level;
|
return level;
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,6 +25,11 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
propTypes: {
|
propTypes: {
|
||||||
value: React.PropTypes.number.isRequired,
|
value: React.PropTypes.number.isRequired,
|
||||||
|
// The maximum value that can be set with the power selector
|
||||||
|
maxValue: React.PropTypes.number.isRequired,
|
||||||
|
|
||||||
|
// Default user power level for the room
|
||||||
|
usersDefault: React.PropTypes.number.isRequired,
|
||||||
|
|
||||||
// if true, the <select/> should be a 'controlled' form element and updated by React
|
// if true, the <select/> should be a 'controlled' form element and updated by React
|
||||||
// to reflect the current value, rather than left freeform.
|
// to reflect the current value, rather than left freeform.
|
||||||
|
@ -41,63 +46,83 @@ module.exports = React.createClass({
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
return {
|
return {
|
||||||
levelRoleMap: {},
|
levelRoleMap: {},
|
||||||
|
// List of power levels to show in the drop-down
|
||||||
|
options: [],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
getDefaultProps: function() {
|
||||||
|
return {
|
||||||
|
maxValue: Infinity,
|
||||||
|
usersDefault: 0,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillMount: function() {
|
componentWillMount: function() {
|
||||||
|
this._initStateFromProps(this.props);
|
||||||
|
},
|
||||||
|
|
||||||
|
componentWillReceiveProps: function(newProps) {
|
||||||
|
this._initStateFromProps(newProps);
|
||||||
|
},
|
||||||
|
|
||||||
|
_initStateFromProps: function(newProps) {
|
||||||
// This needs to be done now because levelRoleMap has translated strings
|
// This needs to be done now because levelRoleMap has translated strings
|
||||||
const levelRoleMap = Roles.levelRoleMap();
|
const levelRoleMap = Roles.levelRoleMap(newProps.usersDefault);
|
||||||
|
const options = Object.keys(levelRoleMap).filter((l) => {
|
||||||
|
return l === undefined || l <= newProps.maxValue;
|
||||||
|
});
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
levelRoleMap,
|
levelRoleMap,
|
||||||
custom: levelRoleMap[this.props.value] === undefined,
|
options,
|
||||||
|
custom: levelRoleMap[newProps.value] === undefined,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
onSelectChange: function(event) {
|
onSelectChange: function(event) {
|
||||||
this.setState({ custom: event.target.value === "SELECT_VALUE_CUSTOM" });
|
this.setState({ custom: event.target.value === "SELECT_VALUE_CUSTOM" });
|
||||||
if (event.target.value !== "SELECT_VALUE_CUSTOM") {
|
if (event.target.value !== "SELECT_VALUE_CUSTOM") {
|
||||||
this.props.onChange(this.getValue());
|
this.props.onChange(event.target.value);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onCustomBlur: function(event) {
|
onCustomBlur: function(event) {
|
||||||
this.props.onChange(this.getValue());
|
this.props.onChange(parseInt(this.refs.custom.value));
|
||||||
},
|
},
|
||||||
|
|
||||||
onCustomKeyDown: function(event) {
|
onCustomKeyDown: function(event) {
|
||||||
if (event.key == "Enter") {
|
if (event.key == "Enter") {
|
||||||
this.props.onChange(this.getValue());
|
this.props.onChange(parseInt(this.refs.custom.value));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
getValue: function() {
|
|
||||||
let value;
|
|
||||||
if (this.refs.select) {
|
|
||||||
value = this.refs.select.value;
|
|
||||||
if (this.refs.custom) {
|
|
||||||
if (value === undefined) value = parseInt( this.refs.custom.value );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
},
|
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
let customPicker;
|
let customPicker;
|
||||||
if (this.state.custom) {
|
if (this.state.custom) {
|
||||||
let input;
|
|
||||||
if (this.props.disabled) {
|
if (this.props.disabled) {
|
||||||
input = <span>{ this.props.value }</span>;
|
customPicker = <span>{ _t(
|
||||||
|
"Custom of %(powerLevel)s",
|
||||||
|
{ powerLevel: this.props.value },
|
||||||
|
) }</span>;
|
||||||
} else {
|
} else {
|
||||||
input = <input ref="custom" type="text" size="3" defaultValue={this.props.value} onBlur={this.onCustomBlur} onKeyDown={this.onCustomKeyDown} />;
|
customPicker = <span> = <input
|
||||||
|
ref="custom"
|
||||||
|
type="text"
|
||||||
|
size="3"
|
||||||
|
defaultValue={this.props.value}
|
||||||
|
onBlur={this.onCustomBlur}
|
||||||
|
onKeyDown={this.onCustomKeyDown}
|
||||||
|
/>
|
||||||
|
</span>;
|
||||||
}
|
}
|
||||||
customPicker = <span> of { input }</span>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let selectValue;
|
let selectValue;
|
||||||
if (this.state.custom) {
|
if (this.state.custom) {
|
||||||
selectValue = "SELECT_VALUE_CUSTOM";
|
selectValue = "SELECT_VALUE_CUSTOM";
|
||||||
} else {
|
} else {
|
||||||
selectValue = this.state.levelRoleMap[selectValue] ?
|
selectValue = this.state.levelRoleMap[this.props.value] ?
|
||||||
this.props.value : "SELECT_VALUE_CUSTOM";
|
this.props.value : "SELECT_VALUE_CUSTOM";
|
||||||
}
|
}
|
||||||
let select;
|
let select;
|
||||||
|
@ -105,13 +130,10 @@ module.exports = React.createClass({
|
||||||
select = <span>{ this.state.levelRoleMap[selectValue] }</span>;
|
select = <span>{ this.state.levelRoleMap[selectValue] }</span>;
|
||||||
} else {
|
} else {
|
||||||
// Each level must have a definition in this.state.levelRoleMap
|
// Each level must have a definition in this.state.levelRoleMap
|
||||||
const levels = [0, 50, 100];
|
let options = this.state.options.map((level) => {
|
||||||
let options = levels.map((level) => {
|
|
||||||
return {
|
return {
|
||||||
value: level,
|
value: level,
|
||||||
// Give a userDefault (users_default in the power event) of 0 but
|
text: Roles.textualPowerLevel(level, this.props.usersDefault),
|
||||||
// because level !== undefined, this should never be used.
|
|
||||||
text: Roles.textualPowerLevel(level, 0),
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
options.push({ value: "SELECT_VALUE_CUSTOM", text: _t("Custom level") });
|
options.push({ value: "SELECT_VALUE_CUSTOM", text: _t("Custom level") });
|
||||||
|
|
|
@ -494,7 +494,6 @@ module.exports = withMatrixClient(React.createClass({
|
||||||
const defaultPerms = {
|
const defaultPerms = {
|
||||||
can: {},
|
can: {},
|
||||||
muted: false,
|
muted: false,
|
||||||
modifyLevel: false,
|
|
||||||
};
|
};
|
||||||
const room = this.props.matrixClient.getRoom(member.roomId);
|
const room = this.props.matrixClient.getRoom(member.roomId);
|
||||||
if (!room) return defaultPerms;
|
if (!room) return defaultPerms;
|
||||||
|
@ -516,13 +515,15 @@ module.exports = withMatrixClient(React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
_calculateCanPermissions: function(me, them, powerLevels) {
|
_calculateCanPermissions: function(me, them, powerLevels) {
|
||||||
|
const isMe = me.userId === them.userId;
|
||||||
const can = {
|
const can = {
|
||||||
kick: false,
|
kick: false,
|
||||||
ban: false,
|
ban: false,
|
||||||
mute: false,
|
mute: false,
|
||||||
modifyLevel: false,
|
modifyLevel: false,
|
||||||
|
modifyLevelMax: 0,
|
||||||
};
|
};
|
||||||
const canAffectUser = them.powerLevel < me.powerLevel;
|
const canAffectUser = them.powerLevel < me.powerLevel || isMe;
|
||||||
if (!canAffectUser) {
|
if (!canAffectUser) {
|
||||||
//console.log("Cannot affect user: %s >= %s", them.powerLevel, me.powerLevel);
|
//console.log("Cannot affect user: %s >= %s", them.powerLevel, me.powerLevel);
|
||||||
return can;
|
return can;
|
||||||
|
@ -531,16 +532,13 @@ module.exports = withMatrixClient(React.createClass({
|
||||||
(powerLevels.events ? powerLevels.events["m.room.power_levels"] : null) ||
|
(powerLevels.events ? powerLevels.events["m.room.power_levels"] : null) ||
|
||||||
powerLevels.state_default
|
powerLevels.state_default
|
||||||
);
|
);
|
||||||
const levelToSend = (
|
|
||||||
(powerLevels.events ? powerLevels.events["m.room.message"] : null) ||
|
|
||||||
powerLevels.events_default
|
|
||||||
);
|
|
||||||
|
|
||||||
can.kick = me.powerLevel >= powerLevels.kick;
|
can.kick = me.powerLevel >= powerLevels.kick;
|
||||||
can.ban = me.powerLevel >= powerLevels.ban;
|
can.ban = me.powerLevel >= powerLevels.ban;
|
||||||
can.mute = me.powerLevel >= editPowerLevel;
|
can.mute = me.powerLevel >= editPowerLevel;
|
||||||
can.toggleMod = me.powerLevel > them.powerLevel && them.powerLevel >= levelToSend;
|
can.modifyLevel = me.powerLevel >= editPowerLevel && (isMe || me.powerLevel > them.powerLevel);
|
||||||
can.modifyLevel = me.powerLevel > them.powerLevel && me.powerLevel >= editPowerLevel;
|
can.modifyLevelMax = me.powerLevel;
|
||||||
|
|
||||||
return can;
|
return can;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -832,8 +830,11 @@ module.exports = withMatrixClient(React.createClass({
|
||||||
presenceCurrentlyActive = this.props.member.user.currentlyActive;
|
presenceCurrentlyActive = this.props.member.user.currentlyActive;
|
||||||
}
|
}
|
||||||
|
|
||||||
let roomMemberDetails = null;
|
const room = this.props.matrixClient.getRoom(this.props.member.roomId);
|
||||||
|
const poweLevelEvent = room ? room.currentState.getStateEvents("m.room.power_levels", "") : null;
|
||||||
|
const powerLevelUsersDefault = poweLevelEvent.getContent().users_default;
|
||||||
|
|
||||||
|
let roomMemberDetails = null;
|
||||||
if (this.props.member.roomId) { // is in room
|
if (this.props.member.roomId) { // is in room
|
||||||
const PowerSelector = sdk.getComponent('elements.PowerSelector');
|
const PowerSelector = sdk.getComponent('elements.PowerSelector');
|
||||||
const PresenceLabel = sdk.getComponent('rooms.PresenceLabel');
|
const PresenceLabel = sdk.getComponent('rooms.PresenceLabel');
|
||||||
|
@ -842,7 +843,9 @@ module.exports = withMatrixClient(React.createClass({
|
||||||
{ _t("Level:") } <b>
|
{ _t("Level:") } <b>
|
||||||
<PowerSelector controlled={true}
|
<PowerSelector controlled={true}
|
||||||
value={parseInt(this.props.member.powerLevel)}
|
value={parseInt(this.props.member.powerLevel)}
|
||||||
|
maxValue={this.state.can.modifyLevelMax}
|
||||||
disabled={!this.state.can.modifyLevel}
|
disabled={!this.state.can.modifyLevel}
|
||||||
|
usersDefault={powerLevelUsersDefault}
|
||||||
onChange={this.onPowerChange} />
|
onChange={this.onPowerChange} />
|
||||||
</b>
|
</b>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -910,31 +910,31 @@ module.exports = React.createClass({
|
||||||
<div className="mx_RoomSettings_powerLevels mx_RoomSettings_settings">
|
<div className="mx_RoomSettings_powerLevels mx_RoomSettings_settings">
|
||||||
<div className="mx_RoomSettings_powerLevel">
|
<div className="mx_RoomSettings_powerLevel">
|
||||||
<span className="mx_RoomSettings_powerLevelKey">{ _t('The default role for new room members is') } </span>
|
<span className="mx_RoomSettings_powerLevelKey">{ _t('The default role for new room members is') } </span>
|
||||||
<PowerSelector ref="users_default" value={default_user_level} controlled={false} disabled={!can_change_levels || current_user_level < default_user_level} onChange={this.onPowerLevelsChanged} />
|
<PowerSelector ref="users_default" value={default_user_level} usersDefault={default_user_level} controlled={false} disabled={!can_change_levels || current_user_level < default_user_level} onChange={this.onPowerLevelsChanged} />
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_RoomSettings_powerLevel">
|
<div className="mx_RoomSettings_powerLevel">
|
||||||
<span className="mx_RoomSettings_powerLevelKey">{ _t('To send messages, you must be a') } </span>
|
<span className="mx_RoomSettings_powerLevelKey">{ _t('To send messages, you must be a') } </span>
|
||||||
<PowerSelector ref="events_default" value={send_level} controlled={false} disabled={!can_change_levels || current_user_level < send_level} onChange={this.onPowerLevelsChanged} />
|
<PowerSelector ref="events_default" value={send_level} usersDefault={default_user_level} controlled={false} disabled={!can_change_levels || current_user_level < send_level} onChange={this.onPowerLevelsChanged} />
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_RoomSettings_powerLevel">
|
<div className="mx_RoomSettings_powerLevel">
|
||||||
<span className="mx_RoomSettings_powerLevelKey">{ _t('To invite users into the room, you must be a') } </span>
|
<span className="mx_RoomSettings_powerLevelKey">{ _t('To invite users into the room, you must be a') } </span>
|
||||||
<PowerSelector ref="invite" value={invite_level} controlled={false} disabled={!can_change_levels || current_user_level < invite_level} onChange={this.onPowerLevelsChanged} />
|
<PowerSelector ref="invite" value={invite_level} usersDefault={default_user_level} controlled={false} disabled={!can_change_levels || current_user_level < invite_level} onChange={this.onPowerLevelsChanged} />
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_RoomSettings_powerLevel">
|
<div className="mx_RoomSettings_powerLevel">
|
||||||
<span className="mx_RoomSettings_powerLevelKey">{ _t('To configure the room, you must be a') } </span>
|
<span className="mx_RoomSettings_powerLevelKey">{ _t('To configure the room, you must be a') } </span>
|
||||||
<PowerSelector ref="state_default" value={state_level} controlled={false} disabled={!can_change_levels || current_user_level < state_level} onChange={this.onPowerLevelsChanged} />
|
<PowerSelector ref="state_default" value={state_level} usersDefault={default_user_level} controlled={false} disabled={!can_change_levels || current_user_level < state_level} onChange={this.onPowerLevelsChanged} />
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_RoomSettings_powerLevel">
|
<div className="mx_RoomSettings_powerLevel">
|
||||||
<span className="mx_RoomSettings_powerLevelKey">{ _t('To kick users, you must be a') } </span>
|
<span className="mx_RoomSettings_powerLevelKey">{ _t('To kick users, you must be a') } </span>
|
||||||
<PowerSelector ref="kick" value={kick_level} controlled={false} disabled={!can_change_levels || current_user_level < kick_level} onChange={this.onPowerLevelsChanged} />
|
<PowerSelector ref="kick" value={kick_level} usersDefault={default_user_level} controlled={false} disabled={!can_change_levels || current_user_level < kick_level} onChange={this.onPowerLevelsChanged} />
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_RoomSettings_powerLevel">
|
<div className="mx_RoomSettings_powerLevel">
|
||||||
<span className="mx_RoomSettings_powerLevelKey">{ _t('To ban users, you must be a') } </span>
|
<span className="mx_RoomSettings_powerLevelKey">{ _t('To ban users, you must be a') } </span>
|
||||||
<PowerSelector ref="ban" value={ban_level} controlled={false} disabled={!can_change_levels || current_user_level < ban_level} onChange={this.onPowerLevelsChanged} />
|
<PowerSelector ref="ban" value={ban_level} usersDefault={default_user_level} controlled={false} disabled={!can_change_levels || current_user_level < ban_level} onChange={this.onPowerLevelsChanged} />
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_RoomSettings_powerLevel">
|
<div className="mx_RoomSettings_powerLevel">
|
||||||
<span className="mx_RoomSettings_powerLevelKey">{ _t('To remove other users\' messages, you must be a') } </span>
|
<span className="mx_RoomSettings_powerLevelKey">{ _t('To remove other users\' messages, you must be a') } </span>
|
||||||
<PowerSelector ref="redact" value={redact_level} controlled={false} disabled={!can_change_levels || current_user_level < redact_level} onChange={this.onPowerLevelsChanged} />
|
<PowerSelector ref="redact" value={redact_level} usersDefault={default_user_level} controlled={false} disabled={!can_change_levels || current_user_level < redact_level} onChange={this.onPowerLevelsChanged} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{ Object.keys(events_levels).map(function(event_type, i) {
|
{ Object.keys(events_levels).map(function(event_type, i) {
|
||||||
|
@ -944,7 +944,7 @@ module.exports = React.createClass({
|
||||||
return (
|
return (
|
||||||
<div className="mx_RoomSettings_powerLevel" key={event_type}>
|
<div className="mx_RoomSettings_powerLevel" key={event_type}>
|
||||||
<span className="mx_RoomSettings_powerLevelKey">{ label } </span>
|
<span className="mx_RoomSettings_powerLevelKey">{ label } </span>
|
||||||
<PowerSelector ref={"event_levels_"+event_type} value={events_levels[event_type]} onChange={self.onPowerLevelsChanged}
|
<PowerSelector ref={"event_levels_"+event_type} value={events_levels[event_type]} usersDefault={default_user_level} onChange={self.onPowerLevelsChanged}
|
||||||
controlled={false} disabled={!can_change_levels || current_user_level < events_levels[event_type]} />
|
controlled={false} disabled={!can_change_levels || current_user_level < events_levels[event_type]} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -63,7 +63,7 @@
|
||||||
"This email address was not found": "This email address was not found",
|
"This email address was not found": "This email address was not found",
|
||||||
"Your email address does not appear to be associated with a Matrix ID on this Homeserver.": "Your email address does not appear to be associated with a Matrix ID on this Homeserver.",
|
"Your email address does not appear to be associated with a Matrix ID on this Homeserver.": "Your email address does not appear to be associated with a Matrix ID on this Homeserver.",
|
||||||
"Default": "Default",
|
"Default": "Default",
|
||||||
"User": "User",
|
"Restricted": "Restricted",
|
||||||
"Moderator": "Moderator",
|
"Moderator": "Moderator",
|
||||||
"Admin": "Admin",
|
"Admin": "Admin",
|
||||||
"Start a chat": "Start a chat",
|
"Start a chat": "Start a chat",
|
||||||
|
@ -150,7 +150,6 @@
|
||||||
"%(widgetName)s widget modified by %(senderName)s": "%(widgetName)s widget modified by %(senderName)s",
|
"%(widgetName)s widget modified by %(senderName)s": "%(widgetName)s widget modified by %(senderName)s",
|
||||||
"%(widgetName)s widget added by %(senderName)s": "%(widgetName)s widget added by %(senderName)s",
|
"%(widgetName)s widget added by %(senderName)s": "%(widgetName)s widget added by %(senderName)s",
|
||||||
"%(widgetName)s widget removed by %(senderName)s": "%(widgetName)s widget removed by %(senderName)s",
|
"%(widgetName)s widget removed by %(senderName)s": "%(widgetName)s widget removed by %(senderName)s",
|
||||||
"Communities": "Communities",
|
|
||||||
"Message Pinning": "Message Pinning",
|
"Message Pinning": "Message Pinning",
|
||||||
"%(displayName)s is typing": "%(displayName)s is typing",
|
"%(displayName)s is typing": "%(displayName)s is typing",
|
||||||
"%(names)s and %(count)s others are typing|other": "%(names)s and %(count)s others are typing",
|
"%(names)s and %(count)s others are typing|other": "%(names)s and %(count)s others are typing",
|
||||||
|
@ -525,6 +524,7 @@
|
||||||
"Unverify": "Unverify",
|
"Unverify": "Unverify",
|
||||||
"Verify...": "Verify...",
|
"Verify...": "Verify...",
|
||||||
"No results": "No results",
|
"No results": "No results",
|
||||||
|
"Communities": "Communities",
|
||||||
"Home": "Home",
|
"Home": "Home",
|
||||||
"Integrations Error": "Integrations Error",
|
"Integrations Error": "Integrations Error",
|
||||||
"Could not connect to the integration server": "Could not connect to the integration server",
|
"Could not connect to the integration server": "Could not connect to the integration server",
|
||||||
|
@ -581,6 +581,7 @@
|
||||||
"%(items)s and %(count)s others|other": "%(items)s and %(count)s others",
|
"%(items)s and %(count)s others|other": "%(items)s and %(count)s others",
|
||||||
"%(items)s and %(count)s others|one": "%(items)s and one other",
|
"%(items)s and %(count)s others|one": "%(items)s and one other",
|
||||||
"%(items)s and %(lastItem)s": "%(items)s and %(lastItem)s",
|
"%(items)s and %(lastItem)s": "%(items)s and %(lastItem)s",
|
||||||
|
"Custom of %(powerLevel)s": "Custom of %(powerLevel)s",
|
||||||
"Custom level": "Custom level",
|
"Custom level": "Custom level",
|
||||||
"Room directory": "Room directory",
|
"Room directory": "Room directory",
|
||||||
"Start chat": "Start chat",
|
"Start chat": "Start chat",
|
||||||
|
|
Loading…
Reference in a new issue