[FIX] Watermelon batches (#1277)
This commit is contained in:
parent
08dac6ff86
commit
145e5c6b55
|
@ -91,22 +91,24 @@ class EmojiPicker extends Component {
|
|||
_addFrequentlyUsed = protectedFunction(async(emoji) => {
|
||||
const db = database.active;
|
||||
const freqEmojiCollection = db.collections.get('frequently_used_emojis');
|
||||
await db.action(async() => {
|
||||
let freqEmojiRecord;
|
||||
try {
|
||||
const freqEmojiRecord = await freqEmojiCollection.find(emoji.content);
|
||||
freqEmojiRecord = await freqEmojiCollection.find(emoji.content);
|
||||
} catch (error) {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
await db.action(async() => {
|
||||
if (freqEmojiRecord) {
|
||||
await freqEmojiRecord.update((f) => {
|
||||
f.count += 1;
|
||||
});
|
||||
} catch (error) {
|
||||
try {
|
||||
} else {
|
||||
await freqEmojiCollection.create((f) => {
|
||||
f._raw = sanitizedRaw({ id: emoji.content }, freqEmojiCollection.schema);
|
||||
Object.assign(f, emoji);
|
||||
f.count = 1;
|
||||
});
|
||||
} catch (e) {
|
||||
// Do nothing
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
|
|
|
@ -6,11 +6,13 @@ import RocketChat from '../lib/rocketchat';
|
|||
import database from '../lib/database';
|
||||
import protectedFunction from '../lib/methods/helpers/protectedFunction';
|
||||
import I18n from '../i18n';
|
||||
import log from '../utils/log';
|
||||
|
||||
class MessageErrorActions extends React.Component {
|
||||
static propTypes = {
|
||||
actionsHide: PropTypes.func.isRequired,
|
||||
message: PropTypes.object
|
||||
message: PropTypes.object,
|
||||
tmid: PropTypes.string
|
||||
};
|
||||
|
||||
// eslint-disable-next-line react/sort-comp
|
||||
|
@ -27,17 +29,66 @@ class MessageErrorActions extends React.Component {
|
|||
}
|
||||
|
||||
handleResend = protectedFunction(async() => {
|
||||
const { message } = this.props;
|
||||
await RocketChat.resendMessage(message);
|
||||
const { message, tmid } = this.props;
|
||||
await RocketChat.resendMessage(message, tmid);
|
||||
});
|
||||
|
||||
handleDelete = protectedFunction(async() => {
|
||||
const { message } = this.props;
|
||||
handleDelete = async() => {
|
||||
try {
|
||||
const { message, tmid } = this.props;
|
||||
const db = database.active;
|
||||
await db.action(async() => {
|
||||
await message.destroyPermanently();
|
||||
});
|
||||
const deleteBatch = [];
|
||||
const msgCollection = db.collections.get('messages');
|
||||
const threadCollection = db.collections.get('threads');
|
||||
|
||||
// Delete the object (it can be Message or ThreadMessage instance)
|
||||
deleteBatch.push(message.prepareDestroyPermanently());
|
||||
|
||||
// If it's a thread, we find and delete the whole tree, if necessary
|
||||
if (tmid) {
|
||||
try {
|
||||
const msg = await msgCollection.find(message.id);
|
||||
deleteBatch.push(msg.prepareDestroyPermanently());
|
||||
} catch (error) {
|
||||
// Do nothing: message not found
|
||||
}
|
||||
|
||||
try {
|
||||
// Find the thread header and update it
|
||||
const msg = await msgCollection.find(tmid);
|
||||
if (msg.tcount <= 1) {
|
||||
deleteBatch.push(
|
||||
msg.prepareUpdate((m) => {
|
||||
m.tcount = null;
|
||||
m.tlm = null;
|
||||
})
|
||||
);
|
||||
|
||||
try {
|
||||
// If the whole thread was removed, delete the thread
|
||||
const thread = await threadCollection.find(tmid);
|
||||
deleteBatch.push(thread.prepareDestroyPermanently());
|
||||
} catch (error) {
|
||||
// Do nothing: thread not found
|
||||
}
|
||||
} else {
|
||||
deleteBatch.push(
|
||||
msg.prepareUpdate((m) => {
|
||||
m.tcount -= 1;
|
||||
})
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
// Do nothing: message not found
|
||||
}
|
||||
}
|
||||
await db.action(async() => {
|
||||
await db.batch(...deleteBatch);
|
||||
});
|
||||
} catch (e) {
|
||||
log(e);
|
||||
}
|
||||
}
|
||||
|
||||
showActionSheet = () => {
|
||||
ActionSheet.showActionSheetWithOptions({
|
||||
|
|
|
@ -52,7 +52,7 @@ MessageInner.displayName = 'MessageInner';
|
|||
|
||||
const Message = React.memo((props) => {
|
||||
if (props.isThreadReply || props.isThreadSequential || props.isInfo) {
|
||||
const thread = props.isThreadReply ? <RepliedThread isTemp={props.isTemp} {...props} /> : null;
|
||||
const thread = props.isThreadReply ? <RepliedThread {...props} /> : null;
|
||||
return (
|
||||
<View style={[styles.container, props.style]}>
|
||||
{thread}
|
||||
|
|
|
@ -9,9 +9,9 @@ import DisclosureIndicator from '../DisclosureIndicator';
|
|||
import styles from './styles';
|
||||
|
||||
const RepliedThread = React.memo(({
|
||||
tmid, tmsg, isHeader, isTemp, fetchThreadName, id
|
||||
tmid, tmsg, isHeader, fetchThreadName, id
|
||||
}) => {
|
||||
if (!tmid || !isHeader || isTemp) {
|
||||
if (!tmid || !isHeader) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -40,9 +40,6 @@ const RepliedThread = React.memo(({
|
|||
if (prevProps.isHeader !== nextProps.isHeader) {
|
||||
return false;
|
||||
}
|
||||
if (prevProps.isTemp !== nextProps.isTemp) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
|
@ -51,7 +48,6 @@ RepliedThread.propTypes = {
|
|||
tmsg: PropTypes.string,
|
||||
id: PropTypes.string,
|
||||
isHeader: PropTypes.bool,
|
||||
isTemp: PropTypes.bool,
|
||||
fetchThreadName: PropTypes.func
|
||||
};
|
||||
RepliedThread.displayName = 'MessageRepliedThread';
|
||||
|
|
|
@ -3,12 +3,14 @@ import log from '../../utils/log';
|
|||
|
||||
export default async function readMessages(rid, lastOpen) {
|
||||
try {
|
||||
// RC 0.61.0
|
||||
const data = await this.sdk.post('subscriptions.read', { rid });
|
||||
const db = database.active;
|
||||
const subscription = await db.collections.get('subscriptions').find(rid);
|
||||
|
||||
// RC 0.61.0
|
||||
await this.sdk.post('subscriptions.read', { rid });
|
||||
|
||||
await db.action(async() => {
|
||||
try {
|
||||
const subscription = await db.collections.get('subscriptions').find(rid);
|
||||
await subscription.update((s) => {
|
||||
s.open = true;
|
||||
s.alert = false;
|
||||
|
@ -22,7 +24,6 @@ export default async function readMessages(rid, lastOpen) {
|
|||
// Do nothing
|
||||
}
|
||||
});
|
||||
return data;
|
||||
} catch (e) {
|
||||
log(e);
|
||||
}
|
||||
|
|
|
@ -5,81 +5,155 @@ import database from '../database';
|
|||
import log from '../../utils/log';
|
||||
import random from '../../utils/random';
|
||||
|
||||
export const getMessage = async(rid, msg = '', tmid, user) => {
|
||||
const _id = random(17);
|
||||
const { id, username } = user;
|
||||
try {
|
||||
const db = database.active;
|
||||
const msgCollection = db.collections.get('messages');
|
||||
let message;
|
||||
await db.action(async() => {
|
||||
message = await msgCollection.create((m) => {
|
||||
m._raw = sanitizedRaw({ id: _id }, msgCollection.schema);
|
||||
m.subscription.id = rid;
|
||||
m.msg = msg;
|
||||
m.tmid = tmid;
|
||||
m.ts = new Date();
|
||||
m._updatedAt = new Date();
|
||||
m.status = messagesStatus.TEMP;
|
||||
m.u = {
|
||||
_id: id || '1',
|
||||
username
|
||||
};
|
||||
});
|
||||
});
|
||||
return message;
|
||||
} catch (error) {
|
||||
console.warn('getMessage', error);
|
||||
}
|
||||
};
|
||||
|
||||
export async function sendMessageCall(message) {
|
||||
const {
|
||||
id: _id, subscription: { id: rid }, msg, tmid
|
||||
} = message;
|
||||
try {
|
||||
// RC 0.60.0
|
||||
const data = await this.sdk.post('chat.sendMessage', {
|
||||
await this.sdk.post('chat.sendMessage', {
|
||||
message: {
|
||||
_id, rid, msg, tmid
|
||||
}
|
||||
});
|
||||
return data;
|
||||
} catch (e) {
|
||||
const db = database.active;
|
||||
const msgCollection = db.collections.get('messages');
|
||||
const threadMessagesCollection = db.collections.get('thread_messages');
|
||||
const errorBatch = [];
|
||||
const messageRecord = await msgCollection.find(_id);
|
||||
errorBatch.push(
|
||||
messageRecord.prepareUpdate((m) => {
|
||||
m.status = messagesStatus.ERROR;
|
||||
})
|
||||
);
|
||||
|
||||
if (tmid) {
|
||||
const threadMessageRecord = await threadMessagesCollection.find(_id);
|
||||
errorBatch.push(
|
||||
threadMessageRecord.prepareUpdate((tm) => {
|
||||
tm.status = messagesStatus.ERROR;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
await db.action(async() => {
|
||||
await db.batch(...errorBatch);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default async function(rid, msg, tmid, user) {
|
||||
try {
|
||||
const db = database.active;
|
||||
const subsCollections = db.collections.get('subscriptions');
|
||||
const message = await getMessage(rid, msg, tmid, user);
|
||||
if (!message) {
|
||||
return;
|
||||
}
|
||||
const subsCollection = db.collections.get('subscriptions');
|
||||
const msgCollection = db.collections.get('messages');
|
||||
const threadCollection = db.collections.get('threads');
|
||||
const threadMessagesCollection = db.collections.get('thread_messages');
|
||||
const messageId = random(17);
|
||||
const batch = [];
|
||||
const message = {
|
||||
id: messageId, subscription: { id: rid }, msg, tmid
|
||||
};
|
||||
const messageDate = new Date();
|
||||
let tMessageRecord;
|
||||
|
||||
// If it's replying to a thread
|
||||
if (tmid) {
|
||||
try {
|
||||
// Find thread message header in Messages collection
|
||||
tMessageRecord = await msgCollection.find(tmid);
|
||||
batch.push(
|
||||
tMessageRecord.prepareUpdate((m) => {
|
||||
m.tlm = messageDate;
|
||||
m.tcount += 1;
|
||||
})
|
||||
);
|
||||
|
||||
try {
|
||||
const room = await subsCollections.find(rid);
|
||||
await db.action(async() => {
|
||||
await room.update((r) => {
|
||||
// Find thread message header in Threads collection
|
||||
await threadCollection.find(tmid);
|
||||
} catch (error) {
|
||||
// If there's no record, create one
|
||||
batch.push(
|
||||
threadCollection.prepareCreate((tm) => {
|
||||
tm._raw = sanitizedRaw({ id: tmid }, threadCollection.schema);
|
||||
tm.subscription.id = rid;
|
||||
tm.tmid = tmid;
|
||||
tm.msg = tMessageRecord.msg;
|
||||
tm.ts = tMessageRecord.ts;
|
||||
tm._updatedAt = messageDate;
|
||||
tm.status = messagesStatus.SENT; // Original message was sent already
|
||||
tm.u = tMessageRecord.u;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// Create the message sent in ThreadMessages collection
|
||||
batch.push(
|
||||
threadMessagesCollection.prepareCreate((tm) => {
|
||||
tm._raw = sanitizedRaw({ id: messageId }, threadMessagesCollection.schema);
|
||||
tm.subscription.id = rid;
|
||||
tm.rid = tmid;
|
||||
tm.msg = msg;
|
||||
tm.ts = messageDate;
|
||||
tm._updatedAt = messageDate;
|
||||
tm.status = messagesStatus.TEMP;
|
||||
tm.u = {
|
||||
_id: user.id || '1',
|
||||
username: user.username
|
||||
};
|
||||
})
|
||||
);
|
||||
} catch (e) {
|
||||
log(e);
|
||||
}
|
||||
}
|
||||
|
||||
// Create the message sent in Messages collection
|
||||
batch.push(
|
||||
msgCollection.prepareCreate((m) => {
|
||||
m._raw = sanitizedRaw({ id: messageId }, msgCollection.schema);
|
||||
m.subscription.id = rid;
|
||||
m.msg = msg;
|
||||
m.ts = messageDate;
|
||||
m._updatedAt = messageDate;
|
||||
m.status = messagesStatus.TEMP;
|
||||
m.u = {
|
||||
_id: user.id || '1',
|
||||
username: user.username
|
||||
};
|
||||
if (tmid) {
|
||||
m.tmid = tmid;
|
||||
m.tlm = messageDate;
|
||||
m.tmsg = tMessageRecord.msg;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
try {
|
||||
const room = await subsCollection.find(rid);
|
||||
if (room.draftMessage) {
|
||||
batch.push(
|
||||
room.prepareUpdate((r) => {
|
||||
r.draftMessage = null;
|
||||
});
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
try {
|
||||
await sendMessageCall.call(this, message);
|
||||
await db.action(async() => {
|
||||
await message.update((m) => {
|
||||
m.status = messagesStatus.SENT;
|
||||
});
|
||||
await db.batch(...batch);
|
||||
});
|
||||
} catch (e) {
|
||||
await db.action(async() => {
|
||||
await message.update((m) => {
|
||||
m.status = messagesStatus.ERROR;
|
||||
});
|
||||
});
|
||||
log(e);
|
||||
return;
|
||||
}
|
||||
|
||||
await sendMessageCall.call(this, message);
|
||||
} catch (e) {
|
||||
log(e);
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import buildMessage from '../helpers/buildMessage';
|
|||
import database from '../../database';
|
||||
import reduxStore from '../../createStore';
|
||||
import { addUserTyping, removeUserTyping, clearUserTyping } from '../../../actions/usersTyping';
|
||||
import debounce from '../../../utils/debounce';
|
||||
|
||||
const unsubscribe = subscriptions => subscriptions.forEach(sub => sub.unsubscribe().catch(() => console.log('unsubscribeRoom')));
|
||||
const removeListener = listener => listener.stop();
|
||||
|
@ -85,6 +86,10 @@ export default function subscribeRoom({ rid }) {
|
|||
}
|
||||
});
|
||||
|
||||
const read = debounce((lastOpen) => {
|
||||
this.readMessages(rid, lastOpen);
|
||||
}, 300);
|
||||
|
||||
const handleMessageReceived = protectedFunction((ddpMessage) => {
|
||||
const message = buildMessage(EJSON.fromJSONValue(ddpMessage.fields.args[0]));
|
||||
const lastOpen = new Date();
|
||||
|
@ -94,20 +99,26 @@ export default function subscribeRoom({ rid }) {
|
|||
InteractionManager.runAfterInteractions(async() => {
|
||||
const db = database.active;
|
||||
const batch = [];
|
||||
const subCollection = db.collections.get('subscriptions');
|
||||
const msgCollection = db.collections.get('messages');
|
||||
const threadsCollection = db.collections.get('threads');
|
||||
const threadMessagesCollection = db.collections.get('thread_messages');
|
||||
let messageRecord;
|
||||
let threadRecord;
|
||||
let threadMessageRecord;
|
||||
|
||||
// Create or update message
|
||||
try {
|
||||
const messageRecord = await msgCollection.find(message._id);
|
||||
batch.push(
|
||||
messageRecord.prepareUpdate((m) => {
|
||||
Object.assign(m, message);
|
||||
})
|
||||
);
|
||||
messageRecord = await msgCollection.find(message._id);
|
||||
} catch (error) {
|
||||
// Do nothing
|
||||
}
|
||||
if (messageRecord) {
|
||||
batch.push(
|
||||
messageRecord.prepareUpdate(protectedFunction((m) => {
|
||||
Object.assign(m, message);
|
||||
}))
|
||||
);
|
||||
} else {
|
||||
batch.push(
|
||||
msgCollection.prepareCreate(protectedFunction((m) => {
|
||||
m._raw = sanitizedRaw({ id: message._id }, msgCollection.schema);
|
||||
|
@ -120,13 +131,18 @@ export default function subscribeRoom({ rid }) {
|
|||
// Create or update thread
|
||||
if (message.tlm) {
|
||||
try {
|
||||
const threadRecord = await threadsCollection.find(message._id);
|
||||
batch.push(
|
||||
threadRecord.prepareUpdate((t) => {
|
||||
Object.assign(t, message);
|
||||
})
|
||||
);
|
||||
threadRecord = await threadsCollection.find(message._id);
|
||||
} catch (error) {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
if (threadRecord) {
|
||||
batch.push(
|
||||
threadRecord.prepareUpdate(protectedFunction((t) => {
|
||||
Object.assign(t, message);
|
||||
}))
|
||||
);
|
||||
} else {
|
||||
batch.push(
|
||||
threadsCollection.prepareCreate(protectedFunction((t) => {
|
||||
t._raw = sanitizedRaw({ id: message._id }, threadsCollection.schema);
|
||||
|
@ -140,15 +156,20 @@ export default function subscribeRoom({ rid }) {
|
|||
// Create or update thread message
|
||||
if (message.tmid) {
|
||||
try {
|
||||
const threadMessageRecord = await threadMessagesCollection.find(message._id);
|
||||
threadMessageRecord = await threadMessagesCollection.find(message._id);
|
||||
} catch (error) {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
if (threadMessageRecord) {
|
||||
batch.push(
|
||||
threadMessageRecord.prepareUpdate((tm) => {
|
||||
threadMessageRecord.prepareUpdate(protectedFunction((tm) => {
|
||||
Object.assign(tm, message);
|
||||
tm.rid = message.tmid;
|
||||
delete tm.tmid;
|
||||
})
|
||||
}))
|
||||
);
|
||||
} catch (error) {
|
||||
} else {
|
||||
batch.push(
|
||||
threadMessagesCollection.prepareCreate(protectedFunction((tm) => {
|
||||
tm._raw = sanitizedRaw({ id: message._id }, threadMessagesCollection.schema);
|
||||
|
@ -161,12 +182,7 @@ export default function subscribeRoom({ rid }) {
|
|||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await subCollection.find(rid);
|
||||
this.readMessages(rid, lastOpen);
|
||||
} catch (e) {
|
||||
console.log('Subscription not found. We probably subscribed to a not joined channel. No need to mark as read.');
|
||||
}
|
||||
read(lastOpen);
|
||||
|
||||
try {
|
||||
await db.action(async() => {
|
||||
|
|
|
@ -66,10 +66,10 @@ const createOrUpdateSubscription = async(subscription, room) => {
|
|||
} catch (error) {
|
||||
try {
|
||||
await db.action(async() => {
|
||||
await roomsCollection.create((r) => {
|
||||
await roomsCollection.create(protectedFunction((r) => {
|
||||
r._raw = sanitizedRaw({ id: room._id }, roomsCollection.schema);
|
||||
Object.assign(r, room);
|
||||
});
|
||||
}));
|
||||
});
|
||||
} catch (e) {
|
||||
// Do nothing
|
||||
|
@ -96,19 +96,25 @@ const createOrUpdateSubscription = async(subscription, room) => {
|
|||
|
||||
const tmp = merge(subscription, room);
|
||||
await db.action(async() => {
|
||||
let sub;
|
||||
try {
|
||||
const sub = await subCollection.find(tmp.rid);
|
||||
await sub.update((s) => {
|
||||
Object.assign(s, tmp);
|
||||
});
|
||||
sub = await subCollection.find(tmp.rid);
|
||||
} catch (error) {
|
||||
await subCollection.create((s) => {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
if (sub) {
|
||||
await sub.update(protectedFunction((s) => {
|
||||
Object.assign(s, tmp);
|
||||
}));
|
||||
} else {
|
||||
await subCollection.create(protectedFunction((s) => {
|
||||
s._raw = sanitizedRaw({ id: tmp.rid }, subCollection.schema);
|
||||
Object.assign(s, tmp);
|
||||
if (s.roomUpdatedAt) {
|
||||
s.roomUpdatedAt = new Date();
|
||||
}
|
||||
});
|
||||
}));
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
|
|
|
@ -39,7 +39,7 @@ import loadMessagesForRoom from './methods/loadMessagesForRoom';
|
|||
import loadMissedMessages from './methods/loadMissedMessages';
|
||||
import loadThreadMessages from './methods/loadThreadMessages';
|
||||
|
||||
import sendMessage, { getMessage, sendMessageCall } from './methods/sendMessage';
|
||||
import sendMessage, { sendMessageCall } from './methods/sendMessage';
|
||||
import { sendFileMessage, cancelUpload, isUploadActive } from './methods/sendFileMessage';
|
||||
|
||||
import callJitsi from './methods/callJitsi';
|
||||
|
@ -427,11 +427,10 @@ const RocketChat = {
|
|||
loadMissedMessages,
|
||||
loadMessagesForRoom,
|
||||
loadThreadMessages,
|
||||
getMessage,
|
||||
sendMessage,
|
||||
getRooms,
|
||||
readMessages,
|
||||
async resendMessage(message) {
|
||||
async resendMessage(message, tmid) {
|
||||
const db = database.active;
|
||||
try {
|
||||
await db.action(async() => {
|
||||
|
@ -439,18 +438,21 @@ const RocketChat = {
|
|||
m.status = messagesStatus.TEMP;
|
||||
});
|
||||
});
|
||||
await sendMessageCall.call(this, message);
|
||||
} catch (error) {
|
||||
try {
|
||||
await db.action(async() => {
|
||||
await message.update((m) => {
|
||||
m.status = messagesStatus.ERROR;
|
||||
});
|
||||
});
|
||||
let m = {
|
||||
id: message.id,
|
||||
msg: message.msg,
|
||||
subscription: { id: message.subscription.id }
|
||||
};
|
||||
if (tmid) {
|
||||
m = {
|
||||
...m,
|
||||
tmid
|
||||
};
|
||||
}
|
||||
await sendMessageCall.call(this, m);
|
||||
} catch (e) {
|
||||
log(e);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async search({ text, filterUsers = true, filterRooms = true }) {
|
||||
|
|
|
@ -775,6 +775,7 @@ class RoomView extends React.Component {
|
|||
}
|
||||
{showErrorActions ? (
|
||||
<MessageErrorActions
|
||||
tmid={this.tmid}
|
||||
message={selectedMessage}
|
||||
actionsHide={this.onErrorActionsHide}
|
||||
/>
|
||||
|
|
|
@ -198,7 +198,13 @@ class ShareListView extends React.Component {
|
|||
const serversCollection = serversDB.collections.get('servers');
|
||||
this.servers = await serversCollection.query().fetch();
|
||||
this.chats = this.data.slice(0, LIMIT);
|
||||
const serverInfo = await serversCollection.find(server);
|
||||
let serverInfo = {};
|
||||
try {
|
||||
serverInfo = await serversCollection.find(server);
|
||||
} catch (error) {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
const canUploadFileResult = canUploadFile(fileInfo || fileData, serverInfo);
|
||||
|
||||
this.internalSetState({
|
||||
|
|
Loading…
Reference in New Issue