Merge pull request #1 from nomadic-labs/async_implementation

working on slow connections
This commit is contained in:
Sharon 2020-04-17 18:28:16 -04:00 committed by GitHub
commit a5759ab587
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 280 additions and 187 deletions

View File

@ -16,7 +16,14 @@
"babel": { "babel": {
"presets": [ "presets": [
"airbnb", "airbnb",
"@babel/preset-env", [
"@babel/preset-env",
{
"targets": {
"node": "12"
}
}
],
"@babel/preset-react" "@babel/preset-react"
], ],
"plugins": [ "plugins": [

View File

@ -274,7 +274,6 @@
.buttons { .buttons {
display: flex; display: flex;
align-items: center; align-items: center;
font-size: 1rem;
button { button {
background-color: transparent; background-color: transparent;

View File

@ -22,7 +22,7 @@ import './styles.scss';
const ENCRYPTION_CONFIG = { "algorithm": "m.megolm.v1.aes-sha2" }; const ENCRYPTION_CONFIG = { "algorithm": "m.megolm.v1.aes-sha2" };
const ENCRYPTION_NOTICE = "Messages in this chat are secured with end-to-end encryption." const ENCRYPTION_NOTICE = "Messages in this chat are secured with end-to-end encryption."
const UNENCRYPTION_NOTICE = "End-to-end message encryption is not available on this browser." const UNENCRYPTION_NOTICE = "Messages in this chat are not encrypted."
const RESTARTING_UNENCRYPTED_CHAT_MESSAGE = "Restarting chat without encryption." const RESTARTING_UNENCRYPTED_CHAT_MESSAGE = "Restarting chat without encryption."
const DEFAULT_MATRIX_SERVER = "https://matrix.rhok.space/" const DEFAULT_MATRIX_SERVER = "https://matrix.rhok.space/"
@ -56,6 +56,10 @@ class ChatBox extends React.Component {
typingStatus: null, typingStatus: null,
awaitingAgreement: true, awaitingAgreement: true,
emojiSelectorOpen: false, emojiSelectorOpen: false,
facilitatorInvited: false,
isMobile: true,
isSlowConnection: true,
decryptionErrors: {},
} }
this.state = this.initialState this.state = this.initialState
this.chatboxInput = React.createRef(); this.chatboxInput = React.createRef();
@ -63,6 +67,37 @@ class ChatBox extends React.Component {
this.termsUrl = React.createRef(); this.termsUrl = React.createRef();
} }
detectMobile = () => {
let isMobile = false;
if ( /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) ) {
console.log('navigator.userAgent', navigator.userAgent)
isMobile = true;
}
if (screen.width < 767) {
console.log('screen.width', screen.width)
isMobile = true;
}
this.setState({ isMobile })
}
detectSlowConnection = () => {
let isSlowConnection = false;
const connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
if (typeof connection !== 'undefined' || connection === null) {
const connectionType = connection.effectiveType;
const slowConnections = ['slow-2g', '2g']
isSlowConnection = slowConnections.includes(connectionType)
}
this.setState({ isSlowConnection })
}
handleToggleOpen = () => { handleToggleOpen = () => {
this.setState((prev) => { this.setState((prev) => {
let { showDock } = prev; let { showDock } = prev;
@ -107,117 +142,120 @@ class ChatBox extends React.Component {
} }
} }
exitChat = () => { exitChat = async () => {
if (!this.state.client) return null; if (!this.state.client) return null;
return this.state.client.leave(this.state.roomId)
.then(() => { await this.state.client.leave(this.state.roomId)
const auth = {
type: 'm.login.password', const auth = {
type: 'm.login.password',
user: this.state.userId,
identifier: {
type: "m.id.user",
user: this.state.userId, user: this.state.userId,
identifier: { },
type: "m.id.user", password: this.state.password,
user: this.state.userId, };
},
password: this.state.password, await this.state.client.deactivateAccount(auth, true)
}; await this.state.client.stopClient()
this.state.client.deactivateAccount(auth, true) await this.state.client.clearStores()
})
.then(() => this.state.client.stopClient()) this.state.localStorage.clear()
.then(() => this.state.client.clearStores()) this.setState(this.initialState)
.then(() => {
this.state.localStorage.clear()
this.setState(this.initialState)
})
} }
initializeChat = () => { createLocalStorage = async (deviceId, sessionId) => {
this.setState({ ready: false }) let localStorage = global.localStorage;
let client; if (typeof localStorage === "undefined" || localStorage === null) {
const deviceDesc = `matrix-chat-${deviceId}-${sessionId}`
const localStoragePath = path.resolve(path.join(os.homedir(), ".local-storage", deviceDesc))
localStorage = new LocalStorage(localStoragePath);
}
return localStorage;
}
createClientWithAccount = async () => {
const tmpClient = matrix.createClient(this.props.matrixServerUrl)
try { try {
client = matrix.createClient(this.props.matrixServerUrl) await tmpClient.registerRequest({})
} catch(error) { } catch(err) {
console.log("Error creating client", error) const username = uuid()
return this.handleInitError(err) const password = uuid()
} const sessionId = err.data.session
// empty registration request to get session const account = await tmpClient.registerRequest({
return client.registerRequest({}) auth: {session: sessionId, type: "m.login.dummy"},
.then(data => { inhibit_login: false,
console.log("Empty registration request to get session", data) password: password,
username: username,
x_show_msisdn: true,
}) })
.catch(err => {
// actual registration request with randomly generated username and password
const username = uuid()
const password = uuid()
const sessionId = err.data.session
client.registerRequest({
auth: {session: sessionId, type: "m.login.dummy"},
inhibit_login: false,
password: password,
username: username,
x_show_msisdn: true,
})
.then(data => {
// use node localStorage if window.localStorage is not available const localStorage = await this.createLocalStorage(account.device_id, sessionId)
let localStorage = global.localStorage;
if (typeof localStorage === "undefined" || localStorage === null) {
const deviceDesc = `matrix-chat-${data.device_id}-${sessionId}`
const localStoragePath = path.resolve(path.join(os.homedir(), ".local-storage", deviceDesc))
localStorage = new LocalStorage(localStoragePath);
}
this.setState({ this.setState({
accessToken: data.access_token, accessToken: account.access_token,
userId: data.user_id, userId: account.user_id,
username: username, username: username,
password: password, password: password,
localStorage: localStorage, localStorage: localStorage,
sessionId: sessionId, sessionId: sessionId,
deviceId: data.device_id, deviceId: account.device_id,
}) })
// create new client with full options let opts = {
let opts = { baseUrl: this.props.matrixServerUrl,
baseUrl: this.props.matrixServerUrl, accessToken: account.access_token,
accessToken: data.access_token, userId: account.user_id,
userId: data.user_id, deviceId: account.device_id,
deviceId: data.device_id, sessionStore: new matrix.WebStorageSessionStore(localStorage),
sessionStore: new matrix.WebStorageSessionStore(localStorage), }
}
client = matrix.createClient(opts) return matrix.createClient(opts)
}) }
.catch(err => {
this.handleInitError(err)
})
.then(() => client.initCrypto())
.catch(err => {
client.stopClient()
client.clearStores()
return Promise.reject({ error: "Failed crypto", message: err })
})
.then(() => client.setDisplayName(this.props.anonymousDisplayName))
.then(() => client.startClient())
.then(() => {
this.setState({
client: client
})
})
.catch(err => {
if (err.error === "Failed crypto") {
this.initializeUnencryptedChat()
} else {
this.handleInitError(err)
}
})
})
} }
initializeUnencryptedChat = () => { initializeChat = async () => {
this.setState({ ready: false }) this.setState({ ready: false })
const client = await this.createClientWithAccount()
this.setState({
client: client
})
client.setDisplayName(this.props.anonymousDisplayName)
this.setMatrixListeners(client)
try {
await client.initCrypto()
} catch(err) {
return this.initializeUnencryptedChat()
}
await client.startClient()
await this.createRoom(client)
}
initializeUnencryptedChat = async () => {
if (this.state.client) {
this.state.client.stopClient()
this.state.client.clearStores()
this.state.localStorage.clear()
}
this.setState({
ready: false,
facilitatorInvited: false,
decryptionErrors: {},
roomId: null,
typingStatus: null,
client: null,
isCryptoEnabled: false,
})
this.displayBotMessage({ body: RESTARTING_UNENCRYPTED_CHAT_MESSAGE })
let opts = { let opts = {
baseUrl: this.props.matrixServerUrl, baseUrl: this.props.matrixServerUrl,
accessToken: this.state.accessToken, accessToken: this.state.accessToken,
@ -226,20 +264,22 @@ class ChatBox extends React.Component {
} }
let client; let client;
client = matrix.createClient(opts)
this.setState({
client: client,
})
try { try {
client = matrix.createClient(opts) this.setMatrixListeners(client)
client.setDisplayName(this.props.anonymousDisplayName) client.setDisplayName(this.props.anonymousDisplayName)
} catch { await client.startClient()
return this.handleInitError(err) await this.createRoom(client)
this.displayBotMessage({ body: UNENCRYPTION_NOTICE })
} catch(err) {
console.log("error", err)
this.handleInitError(err)
} }
return client.startClient()
.then(() => {
this.setState({
client: client,
isCryptoEnabled: false,
})
})
.catch(err => this.handleInitError(err))
} }
handleInitError = (err) => { handleInitError = (err) => {
@ -248,27 +288,36 @@ class ChatBox extends React.Component {
this.setState({ ready: true }) this.setState({ ready: true })
} }
handleDecryptionError = () => { handleDecryptionError = async (event, err) => {
this.displayBotMessage({ body: RESTARTING_UNENCRYPTED_CHAT_MESSAGE }) if (this.state.client) {
const isCryptoEnabled = await this.state.client.isCryptoEnabled()
const isRoomEncrypted = this.state.client.isRoomEncrypted(this.state.roomId)
this.state.client.leave(this.state.roomId) if (!isCryptoEnabled || !isRoomEncrypted) {
.then(() => this.state.client.stopClient()) return this.initializeUnencryptedChat()
.then(() => this.state.client.clearStores()) }
.then(() => this.initializeUnencryptedChat()) }
const eventId = event.getId()
this.displayFakeMessage({ body: '** Unable to decrypt message **' }, event.getSender(), eventId)
this.setState({ decryptionErrors: { [eventId]: true }})
} }
verifyAllRoomDevices = async function(roomId) { verifyAllRoomDevices = async (client, room) => {
let room = this.state.client.getRoom(roomId); if (!room) return;
if (!client) return;
if (!this.state.isCryptoEnabled) return;
let members = (await room.getEncryptionTargetMembers()).map(x => x["userId"]) let members = (await room.getEncryptionTargetMembers()).map(x => x["userId"])
let memberkeys = await this.state.client.downloadKeys(members); let memberkeys = await client.downloadKeys(members);
for (const userId in memberkeys) { for (const userId in memberkeys) {
for (const deviceId in memberkeys[userId]) { for (const deviceId in memberkeys[userId]) {
await this.state.client.setDeviceVerified(userId, deviceId); await client.setDeviceVerified(userId, deviceId);
} }
} }
} }
createRoom = async function() { createRoom = async (client) => {
const currentDate = new Date() const currentDate = new Date()
const chatDate = currentDate.toLocaleDateString() const chatDate = currentDate.toLocaleDateString()
const chatTime = currentDate.toLocaleTimeString() const chatTime = currentDate.toLocaleTimeString()
@ -279,7 +328,7 @@ class ChatBox extends React.Component {
name: `${chatTime}, ${chatDate} - ${this.props.roomName}`, name: `${chatTime}, ${chatDate} - ${this.props.roomName}`,
} }
const isCryptoEnabled = await this.state.client.isCryptoEnabled() const isCryptoEnabled = await client.isCryptoEnabled()
if (isCryptoEnabled) { if (isCryptoEnabled) {
roomConfig.initial_state = [ roomConfig.initial_state = [
@ -291,17 +340,9 @@ class ChatBox extends React.Component {
] ]
} }
const { room_id } = await this.state.client.createRoom(roomConfig) const { room_id } = await client.createRoom(roomConfig)
this.state.client.setPowerLevel(room_id, this.props.botId, 100) client.setPowerLevel(room_id, this.props.botId, 100)
if (isCryptoEnabled) {
this.verifyAllRoomDevices(room_id)
} else {
this.displayBotMessage({ body: UNENCRYPTION_NOTICE })
}
this.displayBotMessage({ body: this.props.confirmationMessage })
this.setState({ this.setState({
roomId: room_id, roomId: room_id,
@ -310,6 +351,9 @@ class ChatBox extends React.Component {
} }
sendMessage = (message) => { sendMessage = (message) => {
if (!this.state.client) {
return null
}
this.state.client.sendTextMessage(this.state.roomId, message) this.state.client.sendTextMessage(this.state.roomId, message)
.catch((err) => { .catch((err) => {
switch (err["name"]) { switch (err["name"]) {
@ -328,10 +372,10 @@ class ChatBox extends React.Component {
}) })
} }
displayFakeMessage = (content, sender) => { displayFakeMessage = (content, sender, messageId=uuid()) => {
const msgList = [...this.state.messages] const msgList = [...this.state.messages]
const msg = { const msg = {
id: uuid(), id: messageId,
type: 'm.room.message', type: 'm.room.message',
sender: sender, sender: sender,
roomId: this.state.roomId, roomId: this.state.roomId,
@ -343,7 +387,6 @@ class ChatBox extends React.Component {
} }
displayBotMessage = (content, roomId) => { displayBotMessage = (content, roomId) => {
console.log('BOT MESSAGE', content)
const msgList = [...this.state.messages] const msgList = [...this.state.messages]
const msg = { const msg = {
id: uuid(), id: uuid(),
@ -353,7 +396,6 @@ class ChatBox extends React.Component {
content: content, content: content,
} }
msgList.push(msg) msgList.push(msg)
console.log(msgList)
this.setState({ messages: msgList }) this.setState({ messages: msgList })
} }
@ -375,9 +417,20 @@ class ChatBox extends React.Component {
return; return;
} }
// check for decryption error message and replace with decrypted message
// or push message to messages array
const messages = [...this.state.messages] const messages = [...this.state.messages]
messages.push(message) const decryptionErrors = {...this.state.decryptionErrors}
this.setState({ messages }) delete decryptionErrors[message.id]
const existingMessageIndex = messages.findIndex(({ id }) => id === message.id)
if (existingMessageIndex > -1) {
messages.splice(existingMessageIndex, 1, message)
} else {
messages.push(message)
}
this.setState({ messages, decryptionErrors })
} }
@ -394,53 +447,70 @@ class ChatBox extends React.Component {
} }
} }
setMatrixListeners = client => {
client.once('sync', (state, prevState, res) => {
if (state === "PREPARED") {
this.setState({ ready: true })
}
});
client.on("Room.timeline", (event, room) => {
if (event.getType() === "m.room.encryption") {
this.displayBotMessage({ body: ENCRYPTION_NOTICE }, room.room_id)
this.verifyAllRoomDevices(client, room)
}
if (event.getType() === "m.room.message" && !this.state.isCryptoEnabled) {
if (event.isEncrypted()) {
return;
}
this.handleMessageEvent(event)
}
if (event.getType() === "m.room.member" && event.getSender() === this.props.botId && event.getContent().membership === "invite") {
this.setState({ facilitatorInvited: true })
}
if (event.getType() === "m.room.member" && event.getSender() !== this.props.botId && event.getContent().membership === "join") {
this.verifyAllRoomDevices(client, room)
}
});
client.on("Event.decrypted", (event, err) => {
if (err) {
return this.handleDecryptionError(event, err)
}
if (event.getType() === "m.room.message") {
this.handleMessageEvent(event)
}
});
client.on("RoomMember.typing", (event, member) => {
if (member.typing && member.roomId === this.state.roomId) {
this.setState({ typingStatus: `${member.name} is typing...` })
}
else {
this.setState({ typingStatus: null })
}
});
}
componentDidUpdate(prevProps, prevState) { componentDidUpdate(prevProps, prevState) {
if (this.state.client && prevState.client !== this.state.client) {
this.createRoom()
this.state.client.once('sync', (state, prevState, res) => {
if (state === "PREPARED") {
this.setState({ ready: true })
}
});
this.state.client.on("Room.timeline", (event, room, toStartOfTimeline) => {
if (event.getType() === "m.room.encryption") {
this.displayBotMessage({ body: ENCRYPTION_NOTICE }, room.room_id)
}
if (event.getType() === "m.room.message" && !this.state.isCryptoEnabled) {
if (event.isEncrypted()) {
return;
}
this.handleMessageEvent(event)
}
});
this.state.client.on("Event.decrypted", (event, err) => {
if (err) {
return this.handleDecryptionError()
}
if (event.getType() === "m.room.message") {
this.handleMessageEvent(event)
}
});
this.state.client.on("RoomMember.typing", (event, member) => {
if (member.typing && member.roomId === this.state.roomId) {
this.setState({ typingStatus: `${member.name} is typing...` })
}
else {
this.setState({ typingStatus: null })
}
});
}
if (prevState.messages.length !== this.state.messages.length) { if (prevState.messages.length !== this.state.messages.length) {
if (this.messageWindow.current.scrollTo) { if (this.messageWindow.current.scrollTo) {
this.messageWindow.current.scrollTo(0, this.messageWindow.current.scrollHeight) this.messageWindow.current.scrollTo(0, this.messageWindow.current.scrollHeight)
} }
} }
if (!prevState.facilitatorInvited && this.state.facilitatorInvited) {
this.displayBotMessage({ body: this.props.confirmationMessage })
}
if (!prevState.opened && this.state.opened) {
this.detectMobile()
// not sure what to do with this
// this.detectSlowConnection()
}
} }
componentDidMount() { componentDidMount() {
@ -460,7 +530,11 @@ class ChatBox extends React.Component {
handleAcceptTerms = () => { handleAcceptTerms = () => {
this.setState({ awaitingAgreement: false }) this.setState({ awaitingAgreement: false })
this.initializeChat() try {
this.initializeChat()
} catch(err) {
this.handleInitError(err)
}
} }
handleRejectTerms = () => { handleRejectTerms = () => {
@ -474,7 +548,6 @@ class ChatBox extends React.Component {
if (!Boolean(message)) return null; if (!Boolean(message)) return null;
if (this.state.client && this.state.roomId) { if (this.state.client && this.state.roomId) {
console.log("Setting state to empty")
this.setState({ inputValue: "" }) this.setState({ inputValue: "" })
this.chatboxInput.current.focus() this.chatboxInput.current.focus()
return this.sendMessage(message) return this.sendMessage(message)
@ -491,7 +564,7 @@ class ChatBox extends React.Component {
} }
render() { render() {
const { ready, messages, inputValue, userId, roomId, typingStatus, opened, showDock, emojiSelectorOpen } = this.state; const { ready, messages, inputValue, userId, roomId, typingStatus, opened, showDock, emojiSelectorOpen, isMobile, decryptionErrors } = this.state;
const inputLabel = 'Send a message...' const inputLabel = 'Send a message...'
return ( return (
@ -532,11 +605,22 @@ class ChatBox extends React.Component {
) )
}) })
} }
{ typingStatus && { typingStatus &&
<div className="notices"> <div className="notices">
<div role="status">{typingStatus}</div> <div role="status">{typingStatus}</div>
</div> </div>
} }
{ Boolean(Object.keys(decryptionErrors).length) &&
<div className={`message from-bot`}>
<div className="text buttons">
{`Restart chat without encryption?`}
<button className="btn" id="accept" onClick={this.initializeUnencryptedChat}>RESTART</button>
</div>
</div>
}
{ !ready && <div className={`loader`}>loading...</div> } { !ready && <div className={`loader`}>loading...</div> }
</div> </div>
</div> </div>
@ -553,12 +637,15 @@ class ChatBox extends React.Component {
autoFocus={true} autoFocus={true}
ref={this.chatboxInput} ref={this.chatboxInput}
/> />
<EmojiSelector {
onEmojiClick={this.onEmojiClick} (status === "entered") && !isMobile &&
emojiSelectorOpen={emojiSelectorOpen} <EmojiSelector
toggleEmojiSelector={this.toggleEmojiSelector} onEmojiClick={this.onEmojiClick}
closeEmojiSelector={this.closeEmojiSelector} emojiSelectorOpen={emojiSelectorOpen}
/> toggleEmojiSelector={this.toggleEmojiSelector}
closeEmojiSelector={this.closeEmojiSelector}
/>
}
</div> </div>
<input type="submit" value="Send" id="submit" onClick={this.handleSubmit} /> <input type="submit" value="Send" id="submit" onClick={this.handleSubmit} />
</form> </form>