Commit 2f5086ea authored by Martin Santangelo's avatar Martin Santangelo

(feat) optimistic messages and move decrypt logic to model

parent 40707251
......@@ -292,6 +292,7 @@
},
"messenger": {
"decrypting": "decrypting...",
"couldntDecrypt":"couldn't decrypt",
"typeYourMessage": "Type your message...",
"invited": "Invited",
"invite": "Invite",
......
/**
* Base model
*/
export default class AbstractModel {
/**
* Child models classes
*/
childModels() {
return {};
}
/**
* Create an instance
* @param {object} data
*/
static create<T extends typeof AbstractModel>(
this: T,
data: object,
): InstanceType<T> {
const obj: InstanceType<T> = new this() as InstanceType<T>;
obj.assign(data);
return obj;
}
/**
* Create an array of instances
* @param {array} arrayData
*/
static createMany<T extends typeof AbstractModel>(
this: T,
arrayData: Array<object>,
): Array<InstanceType<T>> {
const collection: Array<InstanceType<T>> = [];
if (!arrayData) {
return collection;
}
arrayData.forEach((data) => {
const obj: InstanceType<T> = new this() as InstanceType<T>;
obj.assign(data);
collection.push(obj);
});
return collection;
}
/**
* Check if data is an instance of the model and if it is not
* returns a new instance
* @param {object} data
*/
static checkOrCreate<T extends typeof AbstractModel>(
this: T,
data,
): InstanceType<T> {
if (data instanceof this) {
return data as InstanceType<T>;
}
return this.create(data);
}
/**
* Assign values to obj
* @param data any
*/
assign(data: any) {
// Some users have a number as username and engine return them as a number
if (data.username) {
data.username = String(data.username);
}
// some blogs has numeric name
if (data.name) {
data.name = String(data.name);
}
Object.assign(this, data);
// create childs instances
const childs = this.childModels();
for (var prop in childs) {
if (this[prop]) {
this[prop] = childs[prop].create(this[prop]);
}
}
}
}
......@@ -13,11 +13,12 @@ import featuresService from './services/features.service';
import type UserModel from '../channel/UserModel';
import type FeedStore from './stores/FeedStore';
import { showNotification } from '../../AppMessages';
import AbstractModel from './AbstractModel';
/**
* Base model
*/
export default class BaseModel {
export default class BaseModel extends AbstractModel {
username: string = '';
guid: string = '';
ownerObj!: UserModel;
......@@ -81,38 +82,6 @@ export default class BaseModel {
return plainEntity;
}
/**
* Child models classes
*/
childModels() {
return {};
}
/**
* Assign values to obj
* @param data any
*/
assign(data: any) {
// Some users have a number as username and engine return them as a number
if (data.username) {
data.username = String(data.username);
}
// some blogs has numeric name
if (data.name) {
data.name = String(data.name);
}
Object.assign(this, data);
// create childs instances
const childs = this.childModels();
for (var prop in childs) {
if (this[prop]) {
this[prop] = childs[prop].create(this[prop]);
}
}
}
/**
* Return if the current user is the owner of the activity
*/
......@@ -141,56 +110,6 @@ export default class BaseModel {
});
}
/**
* Create an instance
* @param {object} data
*/
static create<T extends typeof BaseModel>(
this: T,
data: object,
): InstanceType<T> {
const obj: InstanceType<T> = new this() as InstanceType<T>;
obj.assign(data);
return obj;
}
/**
* Create an array of instances
* @param {array} arrayData
*/
static createMany<T extends typeof BaseModel>(
this: T,
arrayData: Array<object>,
): Array<InstanceType<T>> {
const collection: Array<InstanceType<T>> = [];
if (!arrayData) {
return collection;
}
arrayData.forEach((data) => {
const obj: InstanceType<T> = new this() as InstanceType<T>;
obj.assign(data);
collection.push(obj);
});
return collection;
}
/**
* Check if data is an instance of the model and if it is not
* returns a new instance
* @param {object} data
*/
static checkOrCreate<T extends typeof BaseModel>(
this: T,
data,
): InstanceType<T> {
if (data instanceof this) {
return data as InstanceType<T>;
}
return this.create(data);
}
/**
* Get a property of the model if it exist or undefined
* @param {string|array} property ex: 'ownerObj.merchant.exclusive.intro'
......
......@@ -78,7 +78,6 @@ class BoostedContentService {
async update() {
await this.feedsService!.fetch();
this.boosts = this.cleanBoosts(await this.feedsService!.getEntities());
console.log(this.boosts);
}
/**
......
//@ts-nocheck
import BaseModel from '../common/BaseModel';
import { observable } from 'mobx';
......@@ -8,6 +7,4 @@ import { observable } from 'mobx';
export default class ConversationModel extends BaseModel {
@observable unread = false;
@observable online = false;
//TODO: move decryption logic here
}
......@@ -35,6 +35,8 @@ import i18n from '../common/services/i18n.service';
import ThemedStyles from '../styles/ThemedStyles';
import isIphoneX from '../common/helpers/isIphoneX';
const keyExtractor = (item) => item.rowKey;
/**
* Messenger Conversation Screen
*/
......@@ -78,16 +80,16 @@ export default class ConversationScreen extends Component {
}
}
this.store.setGuid(conversation.guid);
if (this.props.messengerList.configured) {
this.updateTopAvatar(conversation);
// load conversation
this.store.load().then((conv) => {
// we send the conversation to update the topbar (in case we only receive the guid)
this.updateTopAvatar(conv);
});
}
// load conversation
this.store.setGuid(conversation.guid);
this.store.load().then((conversation) => {
// we send the conversation to update the topbar (in case we only receive the guid)
this.updateTopAvatar(conversation);
});
}
/**
......@@ -165,6 +167,13 @@ export default class ConversationScreen extends Component {
this.updateTopAvatar(conversation);
};
/**
* Set list ref
*/
setRef = (c) => {
this.list = c;
};
/**
* Render component
*/
......@@ -194,7 +203,6 @@ export default class ConversationScreen extends Component {
const footer = this.getFooter();
const messages = this.store.messages.slice();
const conversation = this.props.route.params.conversation;
const avatarImg = {
uri:
MINDS_CDN_URI +
......@@ -207,17 +215,15 @@ export default class ConversationScreen extends Component {
<SafeAreaView style={styles.container}>
<KeyboardAvoidingView
style={[styles.container, ThemedStyles.style.backgroundSecondary]}
behavior={Platform.OS == 'ios' ? 'padding' : null}
behavior={Platform.OS === 'ios' ? 'padding' : null}
keyboardVerticalOffset={isIphoneX ? 100 : 64}>
<FlatList
inverted={true}
data={messages}
ref={(c) => {
this.list = c;
}}
ref={this.setRef}
renderItem={this.renderMessage}
maxToRenderPerBatch={15}
keyExtractor={(item) => item.rowKey}
keyExtractor={keyExtractor}
style={styles.listView}
ListFooterComponent={footer}
windowSize={3}
......@@ -235,7 +241,7 @@ export default class ConversationScreen extends Component {
underlineColorAndroid="transparent"
placeholder={i18n.t('messenger.typeYourMessage')}
placeholderTextColor={ThemedStyles.getColor('secondary_text')}
onChangeText={(text) => this.textChanged(text)}
onChangeText={this.textChanged}
multiline={true}
autogrow={true}
maxHeight={110}
......@@ -264,12 +270,12 @@ export default class ConversationScreen extends Component {
*
* @param { string } text - the text to be checked
*/
textChanged(text) {
textChanged = (text) => {
if (text.length > 180) {
return;
}
this.setState({ text: text });
}
};
/**
* Get list header
......@@ -297,16 +303,17 @@ export default class ConversationScreen extends Component {
* Send message
*/
send = async () => {
const conversationGuid = this.store.guid;
const myGuid = this.props.user.me.guid;
const msg = this.state.text;
this.setState({ text: '' });
try {
const result = await this.store.send(myGuid, msg);
await this.store.send(myGuid, msg);
} catch (err) {
logService.exception('[ConversationScreen]', err);
}
this.setState({ text: '' });
setTimeout(() => {
if (this.list && this.list.scrollToOffset) {
this.list.scrollToOffset({ offset: 0, animated: false });
......@@ -322,7 +329,7 @@ export default class ConversationScreen extends Component {
return (
<Message
message={row.item}
right={row.item.owner.guid == this.props.user.me.guid}
right={row.item.owner.guid === this.props.user.me.guid}
navigation={this.props.navigation}
/>
);
......
......@@ -6,6 +6,7 @@ import crypto from './../common/services/crypto.service';
import socket from '../common/services/socket.service';
import session from '../common/services/session.service';
import logService from '../common/services/log.service';
import MessageModel from './conversation/MessageModel';
/**
* Messenger Conversation Store
......@@ -103,12 +104,12 @@ class MessengerConversationStore {
@action
setMessages(msgs) {
msgs.forEach((m) => this.messages.push(m));
msgs.forEach((m) => this.messages.push(MessageModel.create(m)));
}
@action
addMessage(msg) {
this.messages.unshift(msg);
this.messages.unshift(MessageModel.checkOrCreate(msg));
}
/**
......@@ -118,19 +119,27 @@ class MessengerConversationStore {
* @param {string} text
*/
@action
send(myGuid, text) {
this.messages.unshift({
async send(myGuid, text) {
const message = MessageModel.create({
guid: myGuid + this.messages.length,
rowKey: Date.now().toString(),
message: text,
decrypted: true,
decryptedMessage: text,
sending: true,
owner: { guid: myGuid },
time_created: Date.now() / 1000,
});
return this._encryptMessage(text).then((encrypted) => {
return messengerService.send(this.guid, encrypted);
});
this.messages.unshift(message);
try {
const encrypted = await this._encryptMessage(text);
const response = await messengerService.send(this.guid, encrypted);
message.setSending(false);
message.assign(response.message);
} catch (err) {
console.log(err);
}
}
@action
......@@ -214,9 +223,7 @@ class MessengerConversationStore {
for (let index in message.messages) {
try {
message.message = await crypto.decrypt(message.messages[index]);
message.rowKey = Date.now().toString();
message.decrypted = true;
// break on correct decryption
if (message.message) break;
......
//@ts-nocheck
import React, { PureComponent } from 'react';
import React, { useCallback } from 'react';
import { inject, observer } from 'mobx-react';
import { observer } from 'mobx-react';
import { Text, View, Image, StyleSheet, TouchableOpacity } from 'react-native';
import Icon from 'react-native-vector-icons/Ionicons';
import formatDate from '../../common/helpers/date';
import colors from '../../styles/Colors';
import { CommonStyle } from '../../styles/Common';
import { MINDS_CDN_URI } from '../../config/Config';
import crypto from '../../common/services/crypto.service';
import Tags from '../../common/components/Tags';
import i18n from '../../common/services/i18n.service';
import ThemedStyles from '../../styles/ThemedStyles';
import type MessageModel from './MessageModel';
type PropsType = {
message: MessageModel;
right?: boolean;
navigation: any;
};
/**
* Message Component
* Message
*/
@inject('user')
export default class Message extends PureComponent {
stats = {
showDate: false,
};
state = {
decrypted: false,
msg: i18n.t('messenger.decrypting'),
};
constructor(props) {
super(props);
const { message } = props;
if (message.decrypted) {
this.state = {
decrypted: true,
msg: message.message,
};
}
}
async componentDidMount() {
const message = this.props.message;
if (!this.state.decrypted) {
if (message.message) {
try {
const msg = await crypto.decrypt(message.message);
this.setState({ decrypted: true, msg });
} catch (ex) {
this.setState({ decrypted: true, msg: "couldn't decrypt" });
}
} else {
this.setState({ decrypted: true, msg: '' });
}
}
}
getIcontime(owner) {
if (owner.guid == this.props.user.me.guid)
return '/' + this.props.user.me.icontime;
return '';
}
export default observer(function (props: PropsType) {
const theme = ThemedStyles.style;
/**
* Navigate To channel
*/
_navToChannel = () => {
const navToChannel = useCallback(() => {
// only active if receive the navigation property
if (this.props.navigation) {
this.props.navigation.push('Channel', {
guid: this.props.message.owner.guid,
if (props.navigation && props.message.owner) {
props.navigation.push('Channel', {
guid: props.message.owner.guid,
});
}
};
/**
* Render
*/
render() {
const message = this.props.message;
const avatarImg = {
uri:
MINDS_CDN_URI +
'icon/' +
message.owner.guid +
'/small' +
this.getIcontime(message.owner),
};
if (this.props.right) {
return (
<View>
<View style={[styles.messageContainer, styles.right]}>
<View
style={[
CommonStyle.rowJustifyCenter,
styles.textContainer,
ThemedStyles.style.backgroundLink,
]}>
<Text
selectable={true}
style={[styles.message, CommonStyle.colorWhite]}
onLongPress={() => this.showDate()}>
<Tags
color={'#fff'}
style={{ color: '#FFF' }}
navigation={this.props.navigation}>
{this.state.msg}
</Tags>
</Text>
</View>
<TouchableOpacity onPress={this._navToChannel}>
<Image
source={avatarImg}
style={[styles.avatar, styles.smallavatar]}
/>
</TouchableOpacity>
</View>
{this.state.showDate ? (
<Text
selectable={true}
style={[styles.messagedate, styles.rightText]}>
{formatDate(this.props.message.time_created)}
</Text>
) : null}
</View>
);
}
}, [props.navigation, props.message]);
if (props.right) {
return (
<View>
<View style={styles.messageContainer}>
<TouchableOpacity onPress={this._navToChannel}>
<Image
source={avatarImg}
style={[styles.avatar, styles.smallavatar]}
/>
</TouchableOpacity>
<View
style={[
styles.messageContainer,
styles.right,
props.message.sending ? styles.sending : null,
]}>
<View
style={[
CommonStyle.rowJustifyCenter,
theme.rowJustifyCenter,
styles.textContainer,
,
ThemedStyles.style.backgroundTertiary,
theme.backgroundLink,
]}>
<Text
selectable={true}
style={[styles.message]}
onLongPress={() => this.showDate()}>
<Tags style={[styles.message]} navigation={this.props.navigation}>
{this.state.msg}
style={[styles.message, theme.colorWhite]}
onLongPress={props.message.toggleShowDate}>
<Tags