mirror of
https://github.com/Safe-Support-Chat/ocrcc-chatbox
synced 2024-11-01 00:55:26 +00:00
Merge pull request #1 from nomadic-labs/async_implementation
working on slow connections
This commit is contained in:
commit
a5759ab587
@ -16,7 +16,14 @@
|
||||
"babel": {
|
||||
"presets": [
|
||||
"airbnb",
|
||||
[
|
||||
"@babel/preset-env",
|
||||
{
|
||||
"targets": {
|
||||
"node": "12"
|
||||
}
|
||||
}
|
||||
],
|
||||
"@babel/preset-react"
|
||||
],
|
||||
"plugins": [
|
||||
|
@ -274,7 +274,6 @@
|
||||
.buttons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 1rem;
|
||||
|
||||
button {
|
||||
background-color: transparent;
|
||||
|
@ -22,7 +22,7 @@ import './styles.scss';
|
||||
|
||||
const ENCRYPTION_CONFIG = { "algorithm": "m.megolm.v1.aes-sha2" };
|
||||
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 DEFAULT_MATRIX_SERVER = "https://matrix.rhok.space/"
|
||||
@ -56,6 +56,10 @@ class ChatBox extends React.Component {
|
||||
typingStatus: null,
|
||||
awaitingAgreement: true,
|
||||
emojiSelectorOpen: false,
|
||||
facilitatorInvited: false,
|
||||
isMobile: true,
|
||||
isSlowConnection: true,
|
||||
decryptionErrors: {},
|
||||
}
|
||||
this.state = this.initialState
|
||||
this.chatboxInput = React.createRef();
|
||||
@ -63,6 +67,37 @@ class ChatBox extends React.Component {
|
||||
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 = () => {
|
||||
this.setState((prev) => {
|
||||
let { showDock } = prev;
|
||||
@ -107,10 +142,11 @@ class ChatBox extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
exitChat = () => {
|
||||
exitChat = async () => {
|
||||
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',
|
||||
user: this.state.userId,
|
||||
@ -120,103 +156,105 @@ class ChatBox extends React.Component {
|
||||
},
|
||||
password: this.state.password,
|
||||
};
|
||||
this.state.client.deactivateAccount(auth, true)
|
||||
})
|
||||
.then(() => this.state.client.stopClient())
|
||||
.then(() => this.state.client.clearStores())
|
||||
.then(() => {
|
||||
|
||||
await this.state.client.deactivateAccount(auth, true)
|
||||
await this.state.client.stopClient()
|
||||
await this.state.client.clearStores()
|
||||
|
||||
this.state.localStorage.clear()
|
||||
this.setState(this.initialState)
|
||||
})
|
||||
}
|
||||
|
||||
initializeChat = () => {
|
||||
this.setState({ ready: false })
|
||||
let client;
|
||||
createLocalStorage = async (deviceId, sessionId) => {
|
||||
let localStorage = global.localStorage;
|
||||
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 {
|
||||
client = matrix.createClient(this.props.matrixServerUrl)
|
||||
} catch(error) {
|
||||
console.log("Error creating client", error)
|
||||
return this.handleInitError(err)
|
||||
}
|
||||
|
||||
// empty registration request to get session
|
||||
return client.registerRequest({})
|
||||
.then(data => {
|
||||
console.log("Empty registration request to get session", data)
|
||||
})
|
||||
.catch(err => {
|
||||
// actual registration request with randomly generated username and password
|
||||
await tmpClient.registerRequest({})
|
||||
} catch(err) {
|
||||
const username = uuid()
|
||||
const password = uuid()
|
||||
const sessionId = err.data.session
|
||||
client.registerRequest({
|
||||
|
||||
const account = await tmpClient.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
|
||||
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);
|
||||
}
|
||||
const localStorage = await this.createLocalStorage(account.device_id, sessionId)
|
||||
|
||||
this.setState({
|
||||
accessToken: data.access_token,
|
||||
userId: data.user_id,
|
||||
accessToken: account.access_token,
|
||||
userId: account.user_id,
|
||||
username: username,
|
||||
password: password,
|
||||
localStorage: localStorage,
|
||||
sessionId: sessionId,
|
||||
deviceId: data.device_id,
|
||||
deviceId: account.device_id,
|
||||
})
|
||||
|
||||
// create new client with full options
|
||||
let opts = {
|
||||
baseUrl: this.props.matrixServerUrl,
|
||||
accessToken: data.access_token,
|
||||
userId: data.user_id,
|
||||
deviceId: data.device_id,
|
||||
accessToken: account.access_token,
|
||||
userId: account.user_id,
|
||||
deviceId: account.device_id,
|
||||
sessionStore: new matrix.WebStorageSessionStore(localStorage),
|
||||
}
|
||||
|
||||
client = 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(() => {
|
||||
return matrix.createClient(opts)
|
||||
}
|
||||
}
|
||||
|
||||
initializeChat = async () => {
|
||||
this.setState({ ready: false })
|
||||
|
||||
const client = await this.createClientWithAccount()
|
||||
this.setState({
|
||||
client: client
|
||||
})
|
||||
})
|
||||
.catch(err => {
|
||||
if (err.error === "Failed crypto") {
|
||||
this.initializeUnencryptedChat()
|
||||
} else {
|
||||
this.handleInitError(err)
|
||||
}
|
||||
})
|
||||
})
|
||||
client.setDisplayName(this.props.anonymousDisplayName)
|
||||
this.setMatrixListeners(client)
|
||||
|
||||
try {
|
||||
await client.initCrypto()
|
||||
} catch(err) {
|
||||
return this.initializeUnencryptedChat()
|
||||
}
|
||||
|
||||
initializeUnencryptedChat = () => {
|
||||
this.setState({ ready: false })
|
||||
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 = {
|
||||
baseUrl: this.props.matrixServerUrl,
|
||||
@ -226,20 +264,22 @@ class ChatBox extends React.Component {
|
||||
}
|
||||
|
||||
let client;
|
||||
try {
|
||||
client = matrix.createClient(opts)
|
||||
client.setDisplayName(this.props.anonymousDisplayName)
|
||||
} catch {
|
||||
return this.handleInitError(err)
|
||||
}
|
||||
return client.startClient()
|
||||
.then(() => {
|
||||
this.setState({
|
||||
client: client,
|
||||
isCryptoEnabled: false,
|
||||
})
|
||||
})
|
||||
.catch(err => this.handleInitError(err))
|
||||
|
||||
try {
|
||||
this.setMatrixListeners(client)
|
||||
client.setDisplayName(this.props.anonymousDisplayName)
|
||||
await client.startClient()
|
||||
await this.createRoom(client)
|
||||
this.displayBotMessage({ body: UNENCRYPTION_NOTICE })
|
||||
} catch(err) {
|
||||
console.log("error", err)
|
||||
this.handleInitError(err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
handleInitError = (err) => {
|
||||
@ -248,27 +288,36 @@ class ChatBox extends React.Component {
|
||||
this.setState({ ready: true })
|
||||
}
|
||||
|
||||
handleDecryptionError = () => {
|
||||
this.displayBotMessage({ body: RESTARTING_UNENCRYPTED_CHAT_MESSAGE })
|
||||
handleDecryptionError = async (event, err) => {
|
||||
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)
|
||||
.then(() => this.state.client.stopClient())
|
||||
.then(() => this.state.client.clearStores())
|
||||
.then(() => this.initializeUnencryptedChat())
|
||||
if (!isCryptoEnabled || !isRoomEncrypted) {
|
||||
return this.initializeUnencryptedChat()
|
||||
}
|
||||
}
|
||||
|
||||
verifyAllRoomDevices = async function(roomId) {
|
||||
let room = this.state.client.getRoom(roomId);
|
||||
const eventId = event.getId()
|
||||
this.displayFakeMessage({ body: '** Unable to decrypt message **' }, event.getSender(), eventId)
|
||||
this.setState({ decryptionErrors: { [eventId]: true }})
|
||||
}
|
||||
|
||||
verifyAllRoomDevices = async (client, room) => {
|
||||
if (!room) return;
|
||||
if (!client) return;
|
||||
if (!this.state.isCryptoEnabled) return;
|
||||
|
||||
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 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 chatDate = currentDate.toLocaleDateString()
|
||||
const chatTime = currentDate.toLocaleTimeString()
|
||||
@ -279,7 +328,7 @@ class ChatBox extends React.Component {
|
||||
name: `${chatTime}, ${chatDate} - ${this.props.roomName}`,
|
||||
}
|
||||
|
||||
const isCryptoEnabled = await this.state.client.isCryptoEnabled()
|
||||
const isCryptoEnabled = await client.isCryptoEnabled()
|
||||
|
||||
if (isCryptoEnabled) {
|
||||
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)
|
||||
|
||||
if (isCryptoEnabled) {
|
||||
this.verifyAllRoomDevices(room_id)
|
||||
} else {
|
||||
this.displayBotMessage({ body: UNENCRYPTION_NOTICE })
|
||||
}
|
||||
|
||||
this.displayBotMessage({ body: this.props.confirmationMessage })
|
||||
client.setPowerLevel(room_id, this.props.botId, 100)
|
||||
|
||||
this.setState({
|
||||
roomId: room_id,
|
||||
@ -310,6 +351,9 @@ class ChatBox extends React.Component {
|
||||
}
|
||||
|
||||
sendMessage = (message) => {
|
||||
if (!this.state.client) {
|
||||
return null
|
||||
}
|
||||
this.state.client.sendTextMessage(this.state.roomId, message)
|
||||
.catch((err) => {
|
||||
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 msg = {
|
||||
id: uuid(),
|
||||
id: messageId,
|
||||
type: 'm.room.message',
|
||||
sender: sender,
|
||||
roomId: this.state.roomId,
|
||||
@ -343,7 +387,6 @@ class ChatBox extends React.Component {
|
||||
}
|
||||
|
||||
displayBotMessage = (content, roomId) => {
|
||||
console.log('BOT MESSAGE', content)
|
||||
const msgList = [...this.state.messages]
|
||||
const msg = {
|
||||
id: uuid(),
|
||||
@ -353,7 +396,6 @@ class ChatBox extends React.Component {
|
||||
content: content,
|
||||
}
|
||||
msgList.push(msg)
|
||||
console.log(msgList)
|
||||
|
||||
this.setState({ messages: msgList })
|
||||
}
|
||||
@ -375,9 +417,20 @@ class ChatBox extends React.Component {
|
||||
return;
|
||||
}
|
||||
|
||||
// check for decryption error message and replace with decrypted message
|
||||
// or push message to messages array
|
||||
const messages = [...this.state.messages]
|
||||
const decryptionErrors = {...this.state.decryptionErrors}
|
||||
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 })
|
||||
}
|
||||
|
||||
this.setState({ messages, decryptionErrors })
|
||||
}
|
||||
|
||||
|
||||
@ -394,19 +447,17 @@ class ChatBox extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
if (this.state.client && prevState.client !== this.state.client) {
|
||||
this.createRoom()
|
||||
|
||||
this.state.client.once('sync', (state, prevState, res) => {
|
||||
setMatrixListeners = client => {
|
||||
client.once('sync', (state, prevState, res) => {
|
||||
if (state === "PREPARED") {
|
||||
this.setState({ ready: true })
|
||||
}
|
||||
});
|
||||
|
||||
this.state.client.on("Room.timeline", (event, room, toStartOfTimeline) => {
|
||||
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) {
|
||||
@ -415,18 +466,26 @@ class ChatBox extends React.Component {
|
||||
}
|
||||
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)
|
||||
}
|
||||
});
|
||||
|
||||
this.state.client.on("Event.decrypted", (event, err) => {
|
||||
client.on("Event.decrypted", (event, err) => {
|
||||
if (err) {
|
||||
return this.handleDecryptionError()
|
||||
return this.handleDecryptionError(event, err)
|
||||
}
|
||||
if (event.getType() === "m.room.message") {
|
||||
this.handleMessageEvent(event)
|
||||
}
|
||||
});
|
||||
|
||||
this.state.client.on("RoomMember.typing", (event, member) => {
|
||||
client.on("RoomMember.typing", (event, member) => {
|
||||
if (member.typing && member.roomId === this.state.roomId) {
|
||||
this.setState({ typingStatus: `${member.name} is typing...` })
|
||||
}
|
||||
@ -436,11 +495,22 @@ class ChatBox extends React.Component {
|
||||
});
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
if (prevState.messages.length !== this.state.messages.length) {
|
||||
if (this.messageWindow.current.scrollTo) {
|
||||
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() {
|
||||
@ -460,7 +530,11 @@ class ChatBox extends React.Component {
|
||||
|
||||
handleAcceptTerms = () => {
|
||||
this.setState({ awaitingAgreement: false })
|
||||
try {
|
||||
this.initializeChat()
|
||||
} catch(err) {
|
||||
this.handleInitError(err)
|
||||
}
|
||||
}
|
||||
|
||||
handleRejectTerms = () => {
|
||||
@ -474,7 +548,6 @@ class ChatBox extends React.Component {
|
||||
if (!Boolean(message)) return null;
|
||||
|
||||
if (this.state.client && this.state.roomId) {
|
||||
console.log("Setting state to empty")
|
||||
this.setState({ inputValue: "" })
|
||||
this.chatboxInput.current.focus()
|
||||
return this.sendMessage(message)
|
||||
@ -491,7 +564,7 @@ class ChatBox extends React.Component {
|
||||
}
|
||||
|
||||
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...'
|
||||
|
||||
return (
|
||||
@ -532,11 +605,22 @@ class ChatBox extends React.Component {
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
{ typingStatus &&
|
||||
<div className="notices">
|
||||
<div role="status">{typingStatus}</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> }
|
||||
</div>
|
||||
</div>
|
||||
@ -553,12 +637,15 @@ class ChatBox extends React.Component {
|
||||
autoFocus={true}
|
||||
ref={this.chatboxInput}
|
||||
/>
|
||||
{
|
||||
(status === "entered") && !isMobile &&
|
||||
<EmojiSelector
|
||||
onEmojiClick={this.onEmojiClick}
|
||||
emojiSelectorOpen={emojiSelectorOpen}
|
||||
toggleEmojiSelector={this.toggleEmojiSelector}
|
||||
closeEmojiSelector={this.closeEmojiSelector}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
<input type="submit" value="Send" id="submit" onClick={this.handleSubmit} />
|
||||
</form>
|
||||
|
Loading…
Reference in New Issue
Block a user