[IMPROVEMENT] Tap on avatar/username/channel to show info (#1097)
* added feature to tab on mentions and avtar * fixed lint * removed room param from roomActionView * removed room param from roomActionView * Update tests
This commit is contained in:
parent
80570b0591
commit
857d23ee88
File diff suppressed because it is too large
Load Diff
|
@ -28,6 +28,7 @@ const Content = React.memo((props) => {
|
||||||
numberOfLines={props.tmid ? 1 : 0}
|
numberOfLines={props.tmid ? 1 : 0}
|
||||||
getCustomEmoji={props.getCustomEmoji}
|
getCustomEmoji={props.getCustomEmoji}
|
||||||
useMarkdown={props.useMarkdown}
|
useMarkdown={props.useMarkdown}
|
||||||
|
navToRoomInfo={props.navToRoomInfo}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -50,6 +51,7 @@ Content.propTypes = {
|
||||||
user: PropTypes.object,
|
user: PropTypes.object,
|
||||||
mentions: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
|
mentions: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
|
||||||
channels: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
|
channels: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
|
||||||
|
navToRoomInfo: PropTypes.func,
|
||||||
getCustomEmoji: PropTypes.func
|
getCustomEmoji: PropTypes.func
|
||||||
};
|
};
|
||||||
Content.displayName = 'MessageContent';
|
Content.displayName = 'MessageContent';
|
||||||
|
|
|
@ -48,7 +48,7 @@ const emojiCount = (str) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const Markdown = React.memo(({
|
const Markdown = React.memo(({
|
||||||
msg, style, rules, baseUrl, username, isEdited, numberOfLines, mentions, channels, getCustomEmoji, useMarkdown = true
|
msg, style, rules, baseUrl, username, isEdited, numberOfLines, mentions, channels, getCustomEmoji, useMarkdown = true, navToRoomInfo
|
||||||
}) => {
|
}) => {
|
||||||
if (!msg) {
|
if (!msg) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -92,8 +92,17 @@ const Markdown = React.memo(({
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (mentions && mentions.length && mentions.findIndex(mention => mention.username === content) !== -1) {
|
if (mentions && mentions.length && mentions.findIndex(mention => mention.username === content) !== -1) {
|
||||||
|
const index = mentions.findIndex(mention => mention.username === content);
|
||||||
|
const navParam = {
|
||||||
|
t: 'd',
|
||||||
|
rid: mentions[index]._id
|
||||||
|
};
|
||||||
return (
|
return (
|
||||||
<Text style={mentionStyle} key={key}>
|
<Text
|
||||||
|
style={mentionStyle}
|
||||||
|
key={key}
|
||||||
|
onPress={() => navToRoomInfo(navParam)}
|
||||||
|
>
|
||||||
{content}
|
{content}
|
||||||
</Text>
|
</Text>
|
||||||
);
|
);
|
||||||
|
@ -103,8 +112,17 @@ const Markdown = React.memo(({
|
||||||
hashtag: (node) => {
|
hashtag: (node) => {
|
||||||
const { content, key } = node;
|
const { content, key } = node;
|
||||||
if (channels && channels.length && channels.findIndex(channel => channel.name === content) !== -1) {
|
if (channels && channels.length && channels.findIndex(channel => channel.name === content) !== -1) {
|
||||||
|
const index = channels.findIndex(channel => channel.name === content);
|
||||||
|
const navParam = {
|
||||||
|
t: 'c',
|
||||||
|
rid: channels[index]._id
|
||||||
|
};
|
||||||
return (
|
return (
|
||||||
<Text key={key} style={styles.mention}>
|
<Text
|
||||||
|
key={key}
|
||||||
|
style={styles.mention}
|
||||||
|
onPress={() => navToRoomInfo(navParam)}
|
||||||
|
>
|
||||||
#{content}
|
#{content}
|
||||||
</Text>
|
</Text>
|
||||||
);
|
);
|
||||||
|
@ -161,7 +179,8 @@ Markdown.propTypes = {
|
||||||
useMarkdown: PropTypes.bool,
|
useMarkdown: PropTypes.bool,
|
||||||
mentions: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
|
mentions: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
|
||||||
channels: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
|
channels: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
|
||||||
getCustomEmoji: PropTypes.func
|
getCustomEmoji: PropTypes.func,
|
||||||
|
navToRoomInfo: PropTypes.func
|
||||||
};
|
};
|
||||||
Markdown.displayName = 'MessageMarkdown';
|
Markdown.displayName = 'MessageMarkdown';
|
||||||
|
|
||||||
|
|
|
@ -1,24 +1,34 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
import { TouchableOpacity } from 'react-native';
|
||||||
|
|
||||||
import Avatar from '../Avatar';
|
import Avatar from '../Avatar';
|
||||||
import styles from './styles';
|
import styles from './styles';
|
||||||
|
|
||||||
const MessageAvatar = React.memo(({
|
const MessageAvatar = React.memo(({
|
||||||
isHeader, avatar, author, baseUrl, user, small
|
isHeader, avatar, author, baseUrl, user, small, navToRoomInfo
|
||||||
}) => {
|
}) => {
|
||||||
if (isHeader) {
|
if (isHeader) {
|
||||||
|
const navParam = {
|
||||||
|
t: 'd',
|
||||||
|
rid: author._id
|
||||||
|
};
|
||||||
return (
|
return (
|
||||||
<Avatar
|
<TouchableOpacity
|
||||||
style={small ? styles.avatarSmall : styles.avatar}
|
onPress={() => navToRoomInfo(navParam)}
|
||||||
text={avatar ? '' : author.username}
|
disabled={author._id === user.id}
|
||||||
size={small ? 20 : 36}
|
>
|
||||||
borderRadius={small ? 2 : 4}
|
<Avatar
|
||||||
avatar={avatar}
|
style={small ? styles.avatarSmall : styles.avatar}
|
||||||
baseUrl={baseUrl}
|
text={avatar ? '' : author.username}
|
||||||
userId={user.id}
|
size={small ? 20 : 36}
|
||||||
token={user.token}
|
borderRadius={small ? 2 : 4}
|
||||||
/>
|
avatar={avatar}
|
||||||
|
baseUrl={baseUrl}
|
||||||
|
userId={user.id}
|
||||||
|
token={user.token}
|
||||||
|
/>
|
||||||
|
</TouchableOpacity>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
@ -30,7 +40,8 @@ MessageAvatar.propTypes = {
|
||||||
author: PropTypes.obj,
|
author: PropTypes.obj,
|
||||||
baseUrl: PropTypes.string,
|
baseUrl: PropTypes.string,
|
||||||
user: PropTypes.obj,
|
user: PropTypes.obj,
|
||||||
small: PropTypes.bool
|
small: PropTypes.bool,
|
||||||
|
navToRoomInfo: PropTypes.func
|
||||||
};
|
};
|
||||||
MessageAvatar.displayName = 'MessageAvatar';
|
MessageAvatar.displayName = 'MessageAvatar';
|
||||||
|
|
||||||
|
|
|
@ -39,7 +39,8 @@ export default class MessageContainer extends React.Component {
|
||||||
toggleReactionPicker: PropTypes.func,
|
toggleReactionPicker: PropTypes.func,
|
||||||
fetchThreadName: PropTypes.func,
|
fetchThreadName: PropTypes.func,
|
||||||
onOpenFileModal: PropTypes.func,
|
onOpenFileModal: PropTypes.func,
|
||||||
onReactionLongPress: PropTypes.func
|
onReactionLongPress: PropTypes.func,
|
||||||
|
navToRoomInfo: PropTypes.func
|
||||||
}
|
}
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
|
@ -199,7 +200,7 @@ export default class MessageContainer extends React.Component {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
item, user, style, archived, baseUrl, useRealName, broadcast, fetchThreadName, customThreadTimeFormat, onOpenFileModal, timeFormat, useMarkdown, isReadReceiptEnabled, autoTranslateRoom, autoTranslateLanguage
|
item, user, style, archived, baseUrl, useRealName, broadcast, fetchThreadName, customThreadTimeFormat, onOpenFileModal, timeFormat, useMarkdown, isReadReceiptEnabled, autoTranslateRoom, autoTranslateLanguage, navToRoomInfo
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const {
|
const {
|
||||||
_id, msg, ts, attachments, urls, reactions, t, avatar, u, alias, editedBy, role, drid, dcount, dlm, tmid, tcount, tlm, tmsg, mentions, channels, unread, autoTranslate: autoTranslateMessage
|
_id, msg, ts, attachments, urls, reactions, t, avatar, u, alias, editedBy, role, drid, dcount, dlm, tmid, tcount, tlm, tmsg, mentions, channels, unread, autoTranslate: autoTranslateMessage
|
||||||
|
@ -263,6 +264,7 @@ export default class MessageContainer extends React.Component {
|
||||||
onDiscussionPress={this.onDiscussionPress}
|
onDiscussionPress={this.onDiscussionPress}
|
||||||
onOpenFileModal={onOpenFileModal}
|
onOpenFileModal={onOpenFileModal}
|
||||||
getCustomEmoji={getCustomEmoji}
|
getCustomEmoji={getCustomEmoji}
|
||||||
|
navToRoomInfo={navToRoomInfo}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -640,6 +640,10 @@ const RocketChat = {
|
||||||
// RC 0.48.0
|
// RC 0.48.0
|
||||||
return this.sdk.get('users.info', { userId });
|
return this.sdk.get('users.info', { userId });
|
||||||
},
|
},
|
||||||
|
getRoomInfo(roomId) {
|
||||||
|
// RC 0.72.0
|
||||||
|
return this.sdk.get('rooms.info', { roomId });
|
||||||
|
},
|
||||||
getRoomMemberId(rid, currentUserId) {
|
getRoomMemberId(rid, currentUserId) {
|
||||||
if (rid === `${ currentUserId }${ currentUserId }`) {
|
if (rid === `${ currentUserId }${ currentUserId }`) {
|
||||||
return currentUserId;
|
return currentUserId;
|
||||||
|
|
|
@ -184,7 +184,7 @@ class RoomActionsView extends React.Component {
|
||||||
name: I18n.t('Room_Info'),
|
name: I18n.t('Room_Info'),
|
||||||
route: 'RoomInfoView',
|
route: 'RoomInfoView',
|
||||||
// forward room only if room isn't joined
|
// forward room only if room isn't joined
|
||||||
params: { rid, t, room: joined ? null : room },
|
params: { rid, t },
|
||||||
testID: 'room-actions-info'
|
testID: 'room-actions-info'
|
||||||
}],
|
}],
|
||||||
renderItem: this.renderRoomInfo
|
renderItem: this.renderRoomInfo
|
||||||
|
|
|
@ -9,7 +9,7 @@ import Status from '../../containers/Status';
|
||||||
import Avatar from '../../containers/Avatar';
|
import Avatar from '../../containers/Avatar';
|
||||||
import styles from './styles';
|
import styles from './styles';
|
||||||
import sharedStyles from '../Styles';
|
import sharedStyles from '../Styles';
|
||||||
import database, { safeAddListener } from '../../lib/realm';
|
import database from '../../lib/realm';
|
||||||
import RocketChat from '../../lib/rocketchat';
|
import RocketChat from '../../lib/rocketchat';
|
||||||
import RoomTypeIcon from '../../containers/RoomTypeIcon';
|
import RoomTypeIcon from '../../containers/RoomTypeIcon';
|
||||||
import I18n from '../../i18n';
|
import I18n from '../../i18n';
|
||||||
|
@ -20,8 +20,8 @@ import log from '../../utils/log';
|
||||||
const PERMISSION_EDIT_ROOM = 'edit-room';
|
const PERMISSION_EDIT_ROOM = 'edit-room';
|
||||||
|
|
||||||
const camelize = str => str.replace(/^(.)/, (match, chr) => chr.toUpperCase());
|
const camelize = str => str.replace(/^(.)/, (match, chr) => chr.toUpperCase());
|
||||||
const getRoomTitle = room => (room.t === 'd'
|
const getRoomTitle = (room, type, name) => (type === 'd'
|
||||||
? <Text testID='room-info-view-name' style={styles.roomTitle}>{room.fname}</Text>
|
? <Text testID='room-info-view-name' style={styles.roomTitle}>{name}</Text>
|
||||||
: (
|
: (
|
||||||
<View style={styles.roomTitleRow}>
|
<View style={styles.roomTitleRow}>
|
||||||
<RoomTypeIcon type={room.prid ? 'discussion' : room.t} key='room-info-type' />
|
<RoomTypeIcon type={room.prid ? 'discussion' : room.t} key='room-info-type' />
|
||||||
|
@ -59,28 +59,18 @@ class RoomInfoView extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.rid = props.navigation.getParam('rid');
|
this.rid = props.navigation.getParam('rid');
|
||||||
const room = props.navigation.getParam('room');
|
|
||||||
this.t = props.navigation.getParam('t');
|
this.t = props.navigation.getParam('t');
|
||||||
this.rooms = database.objects('subscriptions').filtered('rid = $0', this.rid);
|
|
||||||
this.roles = database.objects('roles');
|
this.roles = database.objects('roles');
|
||||||
this.sub = {
|
this.sub = {
|
||||||
unsubscribe: () => {}
|
unsubscribe: () => {}
|
||||||
};
|
};
|
||||||
this.state = {
|
this.state = {
|
||||||
room: this.rooms[0] || room || {},
|
room: {},
|
||||||
roomUser: {}
|
roomUser: {}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async componentDidMount() {
|
async componentDidMount() {
|
||||||
safeAddListener(this.rooms, this.updateRoom);
|
|
||||||
const { room } = this.state;
|
|
||||||
const permissions = RocketChat.hasPermission([PERMISSION_EDIT_ROOM], room.rid);
|
|
||||||
if (permissions[PERMISSION_EDIT_ROOM] && !room.prid) {
|
|
||||||
const { navigation } = this.props;
|
|
||||||
navigation.setParams({ showEdit: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.t === 'd') {
|
if (this.t === 'd') {
|
||||||
const { user } = this.props;
|
const { user } = this.props;
|
||||||
const roomUserId = RocketChat.getRoomMemberId(this.rid, user.id);
|
const roomUserId = RocketChat.getRoomMemberId(this.rid, user.id);
|
||||||
|
@ -92,11 +82,30 @@ class RoomInfoView extends React.Component {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log('err_get_user_info', error);
|
log('err_get_user_info', error);
|
||||||
}
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const rooms = database.objects('subscriptions').filtered('rid = $0', this.rid);
|
||||||
|
let room = {};
|
||||||
|
if (rooms.length > 0) {
|
||||||
|
this.setState({ room: rooms[0] });
|
||||||
|
[room] = rooms;
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
const result = await RocketChat.getRoomInfo(this.rid);
|
||||||
|
if (result.success) {
|
||||||
|
// eslint-disable-next-line prefer-destructuring
|
||||||
|
room = result.room;
|
||||||
|
this.setState({ room });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
log('err_get_room_info', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const permissions = RocketChat.hasPermission([PERMISSION_EDIT_ROOM], room.rid);
|
||||||
|
if (permissions[PERMISSION_EDIT_ROOM] && !room.prid) {
|
||||||
|
const { navigation } = this.props;
|
||||||
|
navigation.setParams({ showEdit: true });
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
this.rooms.removeAllListeners();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getRoleDescription = (id) => {
|
getRoleDescription = (id) => {
|
||||||
|
@ -107,10 +116,7 @@ class RoomInfoView extends React.Component {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
isDirect = () => {
|
isDirect = () => this.t === 'd'
|
||||||
const { room: { t } } = this.state;
|
|
||||||
return t === 'd';
|
|
||||||
}
|
|
||||||
|
|
||||||
updateRoom = () => {
|
updateRoom = () => {
|
||||||
if (this.rooms.length > 0) {
|
if (this.rooms.length > 0) {
|
||||||
|
@ -181,15 +187,15 @@ class RoomInfoView extends React.Component {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Avatar
|
<Avatar
|
||||||
text={room.name}
|
text={room.name || roomUser.username}
|
||||||
size={100}
|
size={100}
|
||||||
style={styles.avatar}
|
style={styles.avatar}
|
||||||
type={room.t}
|
type={this.t}
|
||||||
baseUrl={baseUrl}
|
baseUrl={baseUrl}
|
||||||
userId={user.id}
|
userId={user.id}
|
||||||
token={user.token}
|
token={user.token}
|
||||||
>
|
>
|
||||||
{room.t === 'd' && roomUser._id ? <Status style={[sharedStyles.status, styles.status]} size={24} id={roomUser._id} /> : null}
|
{this.t === 'd' && roomUser._id ? <Status style={[sharedStyles.status, styles.status]} size={24} id={roomUser._id} /> : null}
|
||||||
</Avatar>
|
</Avatar>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -231,6 +237,29 @@ class RoomInfoView extends React.Component {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderChannel = () => {
|
||||||
|
const { room } = this.state;
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
{this.renderItem('description', room)}
|
||||||
|
{this.renderItem('topic', room)}
|
||||||
|
{this.renderItem('announcement', room)}
|
||||||
|
{room.broadcast ? this.renderBroadcast() : null}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderDirect = () => {
|
||||||
|
const { roomUser } = this.state;
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
{this.renderRoles()}
|
||||||
|
{this.renderTimezone()}
|
||||||
|
{this.renderCustomFields(roomUser._id)}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { room, roomUser } = this.state;
|
const { room, roomUser } = this.state;
|
||||||
if (!room) {
|
if (!room) {
|
||||||
|
@ -242,15 +271,9 @@ class RoomInfoView extends React.Component {
|
||||||
<SafeAreaView style={styles.container} testID='room-info-view' forceInset={{ vertical: 'never' }}>
|
<SafeAreaView style={styles.container} testID='room-info-view' forceInset={{ vertical: 'never' }}>
|
||||||
<View style={styles.avatarContainer}>
|
<View style={styles.avatarContainer}>
|
||||||
{this.renderAvatar(room, roomUser)}
|
{this.renderAvatar(room, roomUser)}
|
||||||
<View style={styles.roomTitleContainer}>{ getRoomTitle(room) }</View>
|
<View style={styles.roomTitleContainer}>{ getRoomTitle(room, this.t, roomUser && roomUser.name) }</View>
|
||||||
</View>
|
</View>
|
||||||
{!this.isDirect() ? this.renderItem('description', room) : null}
|
{this.isDirect() ? this.renderDirect() : this.renderChannel()}
|
||||||
{!this.isDirect() ? this.renderItem('topic', room) : null}
|
|
||||||
{!this.isDirect() ? this.renderItem('announcement', room) : null}
|
|
||||||
{this.isDirect() ? this.renderRoles() : null}
|
|
||||||
{this.isDirect() ? this.renderTimezone() : null}
|
|
||||||
{this.isDirect() ? this.renderCustomFields(roomUser._id) : null}
|
|
||||||
{room.broadcast ? this.renderBroadcast() : null}
|
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
);
|
);
|
||||||
|
|
|
@ -452,6 +452,14 @@ class RoomView extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
navToRoomInfo = (navParam) => {
|
||||||
|
const { navigation, user } = this.props;
|
||||||
|
if (navParam.rid === user.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
navigation.navigate('RoomInfoView', navParam);
|
||||||
|
}
|
||||||
|
|
||||||
renderItem = (item, previousItem) => {
|
renderItem = (item, previousItem) => {
|
||||||
const { room, lastOpen, canAutoTranslate } = this.state;
|
const { room, lastOpen, canAutoTranslate } = this.state;
|
||||||
const {
|
const {
|
||||||
|
@ -500,6 +508,7 @@ class RoomView extends React.Component {
|
||||||
isReadReceiptEnabled={Message_Read_Receipt_Enabled}
|
isReadReceiptEnabled={Message_Read_Receipt_Enabled}
|
||||||
autoTranslateRoom={canAutoTranslate && room.autoTranslate}
|
autoTranslateRoom={canAutoTranslate && room.autoTranslate}
|
||||||
autoTranslateLanguage={room.autoTranslateLanguage}
|
autoTranslateLanguage={room.autoTranslateLanguage}
|
||||||
|
navToRoomInfo={this.navToRoomInfo}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -8754,7 +8754,7 @@ pad-component@0.0.1:
|
||||||
resolved "https://registry.yarnpkg.com/pad-component/-/pad-component-0.0.1.tgz#ad1f22ce1bf0fdc0d6ddd908af17f351a404b8ac"
|
resolved "https://registry.yarnpkg.com/pad-component/-/pad-component-0.0.1.tgz#ad1f22ce1bf0fdc0d6ddd908af17f351a404b8ac"
|
||||||
integrity sha1-rR8izhvw/cDW3dkIrxfzUaQEuKw=
|
integrity sha1-rR8izhvw/cDW3dkIrxfzUaQEuKw=
|
||||||
|
|
||||||
"paho-mqtt@github:eclipse/paho.mqtt.javascript#master":
|
paho-mqtt@eclipse/paho.mqtt.javascript#master:
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
resolved "https://codeload.github.com/eclipse/paho.mqtt.javascript/tar.gz/f5859463aba9a9b7c19f99ab7c4849a723f8d832"
|
resolved "https://codeload.github.com/eclipse/paho.mqtt.javascript/tar.gz/f5859463aba9a9b7c19f99ab7c4849a723f8d832"
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue