mirror of
https://github.com/cinnyapp/cinny.git
synced 2025-11-14 03:00:29 +03:00
initial commit
This commit is contained in:
commit
026f835a87
176 changed files with 10613 additions and 0 deletions
145
src/client/action/auth.js
Normal file
145
src/client/action/auth.js
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
import * as sdk from 'matrix-js-sdk';
|
||||
import cons from '../state/cons';
|
||||
import { getBaseUrl } from '../../util/matrixUtil';
|
||||
|
||||
async function login(username, homeserver, password) {
|
||||
const baseUrl = await getBaseUrl(homeserver);
|
||||
|
||||
if (typeof baseUrl === 'undefined') throw new Error('Homeserver not found');
|
||||
|
||||
const client = sdk.createClient({ baseUrl });
|
||||
|
||||
const response = await client.login('m.login.password', {
|
||||
user: `@${username}:${homeserver}`,
|
||||
password,
|
||||
initial_device_display_name: cons.DEVICE_DISPLAY_NAME,
|
||||
});
|
||||
|
||||
localStorage.setItem(cons.secretKey.ACCESS_TOKEN, response.access_token);
|
||||
localStorage.setItem(cons.secretKey.DEVICE_ID, response.device_id);
|
||||
localStorage.setItem(cons.secretKey.USER_ID, response.user_id);
|
||||
localStorage.setItem(cons.secretKey.BASE_URL, response.well_known['m.homeserver'].base_url);
|
||||
}
|
||||
|
||||
async function getAdditionalInfo(baseUrl, content) {
|
||||
try {
|
||||
const res = await fetch(`${baseUrl}/_matrix/client/r0/register`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(content),
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8',
|
||||
},
|
||||
credentials: 'same-origin',
|
||||
});
|
||||
const data = await res.json();
|
||||
return data;
|
||||
} catch (e) {
|
||||
throw new Error(e);
|
||||
}
|
||||
}
|
||||
async function verifyEmail(baseUrl, content) {
|
||||
try {
|
||||
const res = await fetch(`${baseUrl}/_matrix/client/r0/register/email/requestToken `, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(content),
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8',
|
||||
},
|
||||
credentials: 'same-origin',
|
||||
});
|
||||
const data = await res.json();
|
||||
return data;
|
||||
} catch (e) {
|
||||
throw new Error(e);
|
||||
}
|
||||
}
|
||||
|
||||
let session = null;
|
||||
let clientSecret = null;
|
||||
let sid = null;
|
||||
async function register(username, homeserver, password, email, recaptchaValue, terms, verified) {
|
||||
const baseUrl = await getBaseUrl(homeserver);
|
||||
|
||||
if (typeof baseUrl === 'undefined') throw new Error('Homeserver not found');
|
||||
|
||||
const client = sdk.createClient({ baseUrl });
|
||||
|
||||
const isAvailable = await client.isUsernameAvailable(username);
|
||||
if (!isAvailable) throw new Error('Username not available');
|
||||
|
||||
if (typeof recaptchaValue === 'string') {
|
||||
await getAdditionalInfo(baseUrl, {
|
||||
auth: {
|
||||
type: 'm.login.recaptcha',
|
||||
session,
|
||||
response: recaptchaValue,
|
||||
},
|
||||
});
|
||||
} else if (terms === true) {
|
||||
await getAdditionalInfo(baseUrl, {
|
||||
auth: {
|
||||
type: 'm.login.terms',
|
||||
session,
|
||||
},
|
||||
});
|
||||
} else if (verified !== true) {
|
||||
session = null;
|
||||
clientSecret = client.generateClientSecret();
|
||||
console.log(clientSecret);
|
||||
const verifyData = await verifyEmail(baseUrl, {
|
||||
email,
|
||||
client_secret: clientSecret,
|
||||
send_attempt: 1,
|
||||
});
|
||||
if (typeof verifyData.error === 'string') {
|
||||
throw new Error(verifyData.error);
|
||||
}
|
||||
sid = verifyData.sid;
|
||||
}
|
||||
|
||||
const additionalInfo = await getAdditionalInfo(baseUrl, {
|
||||
auth: { session: (session !== null) ? session : undefined },
|
||||
});
|
||||
session = additionalInfo.session;
|
||||
if (typeof additionalInfo.completed === 'undefined' || additionalInfo.completed.length === 0) {
|
||||
return ({
|
||||
type: 'recaptcha',
|
||||
public_key: additionalInfo.params['m.login.recaptcha'].public_key,
|
||||
});
|
||||
}
|
||||
if (additionalInfo.completed.find((process) => process === 'm.login.recaptcha') === 'm.login.recaptcha'
|
||||
&& !additionalInfo.completed.find((process) => process === 'm.login.terms')) {
|
||||
return ({
|
||||
type: 'terms',
|
||||
en: additionalInfo.params['m.login.terms'].policies.privacy_policy.en,
|
||||
});
|
||||
}
|
||||
if (verified || additionalInfo.completed.find((process) => process === 'm.login.terms') === 'm.login.terms') {
|
||||
const tpc = {
|
||||
client_secret: clientSecret,
|
||||
sid,
|
||||
};
|
||||
const verifyData = await getAdditionalInfo(baseUrl, {
|
||||
auth: {
|
||||
session,
|
||||
type: 'm.login.email.identity',
|
||||
threepidCreds: tpc,
|
||||
threepid_creds: tpc,
|
||||
},
|
||||
username,
|
||||
password,
|
||||
});
|
||||
if (verifyData.errcode === 'M_UNAUTHORIZED') {
|
||||
return { type: 'email' };
|
||||
}
|
||||
|
||||
localStorage.setItem(cons.secretKey.ACCESS_TOKEN, verifyData.access_token);
|
||||
localStorage.setItem(cons.secretKey.DEVICE_ID, verifyData.device_id);
|
||||
localStorage.setItem(cons.secretKey.USER_ID, verifyData.user_id);
|
||||
localStorage.setItem(cons.secretKey.BASE_URL, baseUrl);
|
||||
return { type: 'done' };
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
export { login, register };
|
||||
12
src/client/action/logout.js
Normal file
12
src/client/action/logout.js
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import initMatrix from '../initMatrix';
|
||||
|
||||
function logout() {
|
||||
const mx = initMatrix.matrixClient;
|
||||
mx.logout().then(() => {
|
||||
mx.clearStores();
|
||||
window.localStorage.clear();
|
||||
window.location.reload();
|
||||
});
|
||||
}
|
||||
|
||||
export default logout;
|
||||
64
src/client/action/navigation.js
Normal file
64
src/client/action/navigation.js
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
import appDispatcher from '../dispatcher';
|
||||
import cons from '../state/cons';
|
||||
|
||||
function handleTabChange(tabId) {
|
||||
appDispatcher.dispatch({
|
||||
type: cons.actions.navigation.CHANGE_TAB,
|
||||
tabId,
|
||||
});
|
||||
}
|
||||
|
||||
function selectRoom(roomId) {
|
||||
appDispatcher.dispatch({
|
||||
type: cons.actions.navigation.SELECT_ROOM,
|
||||
roomId,
|
||||
});
|
||||
}
|
||||
|
||||
function togglePeopleDrawer() {
|
||||
appDispatcher.dispatch({
|
||||
type: cons.actions.navigation.TOGGLE_PEOPLE_DRAWER,
|
||||
});
|
||||
}
|
||||
|
||||
function openInviteList() {
|
||||
appDispatcher.dispatch({
|
||||
type: cons.actions.navigation.OPEN_INVITE_LIST,
|
||||
});
|
||||
}
|
||||
|
||||
function openPublicChannels() {
|
||||
appDispatcher.dispatch({
|
||||
type: cons.actions.navigation.OPEN_PUBLIC_CHANNELS,
|
||||
});
|
||||
}
|
||||
|
||||
function openCreateChannel() {
|
||||
appDispatcher.dispatch({
|
||||
type: cons.actions.navigation.OPEN_CREATE_CHANNEL,
|
||||
});
|
||||
}
|
||||
|
||||
function openInviteUser(roomId) {
|
||||
appDispatcher.dispatch({
|
||||
type: cons.actions.navigation.OPEN_INVITE_USER,
|
||||
roomId,
|
||||
});
|
||||
}
|
||||
|
||||
function openSettings() {
|
||||
appDispatcher.dispatch({
|
||||
type: cons.actions.navigation.OPEN_SETTINGS,
|
||||
});
|
||||
}
|
||||
|
||||
export {
|
||||
handleTabChange,
|
||||
selectRoom,
|
||||
togglePeopleDrawer,
|
||||
openInviteList,
|
||||
openPublicChannels,
|
||||
openCreateChannel,
|
||||
openInviteUser,
|
||||
openSettings,
|
||||
};
|
||||
189
src/client/action/room.js
Normal file
189
src/client/action/room.js
Normal file
|
|
@ -0,0 +1,189 @@
|
|||
import initMatrix from '../initMatrix';
|
||||
import appDispatcher from '../dispatcher';
|
||||
import cons from '../state/cons';
|
||||
|
||||
/**
|
||||
* https://github.com/matrix-org/matrix-react-sdk/blob/1e6c6e9d800890c732d60429449bc280de01a647/src/Rooms.js#L73
|
||||
* @param {string} roomId Id of room to add
|
||||
* @param {string} userId User id to which dm
|
||||
* @returns {Promise} A promise
|
||||
*/
|
||||
function addRoomToMDirect(roomId, userId) {
|
||||
const mx = initMatrix.matrixClient;
|
||||
const mDirectsEvent = mx.getAccountData('m.direct');
|
||||
let userIdToRoomIds = {};
|
||||
|
||||
if (typeof mDirectsEvent !== 'undefined') userIdToRoomIds = mDirectsEvent.getContent();
|
||||
|
||||
// remove it from the lists of any others users
|
||||
// (it can only be a DM room for one person)
|
||||
Object.keys(userIdToRoomIds).forEach((thisUserId) => {
|
||||
const roomIds = userIdToRoomIds[thisUserId];
|
||||
|
||||
if (thisUserId !== userId) {
|
||||
const indexOfRoomId = roomIds.indexOf(roomId);
|
||||
if (indexOfRoomId > -1) {
|
||||
roomIds.splice(indexOfRoomId, 1);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// now add it, if it's not already there
|
||||
if (userId) {
|
||||
const roomIds = userIdToRoomIds[userId] || [];
|
||||
if (roomIds.indexOf(roomId) === -1) {
|
||||
roomIds.push(roomId);
|
||||
}
|
||||
userIdToRoomIds[userId] = roomIds;
|
||||
}
|
||||
|
||||
return mx.setAccountData('m.direct', userIdToRoomIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a room, estimate which of its members is likely to
|
||||
* be the target if the room were a DM room and return that user.
|
||||
* https://github.com/matrix-org/matrix-react-sdk/blob/1e6c6e9d800890c732d60429449bc280de01a647/src/Rooms.js#L117
|
||||
*
|
||||
* @param {Object} room Target room
|
||||
* @param {string} myUserId User ID of the current user
|
||||
* @returns {string} User ID of the user that the room is probably a DM with
|
||||
*/
|
||||
function guessDMRoomTargetId(room, myUserId) {
|
||||
let oldestMemberTs;
|
||||
let oldestMember;
|
||||
|
||||
// Pick the joined user who's been here longest (and isn't us),
|
||||
room.getJoinedMembers().forEach((member) => {
|
||||
if (member.userId === myUserId) return;
|
||||
|
||||
if (typeof oldestMemberTs === 'undefined' || (member.events.member && member.events.member.getTs() < oldestMemberTs)) {
|
||||
oldestMember = member;
|
||||
oldestMemberTs = member.events.member.getTs();
|
||||
}
|
||||
});
|
||||
if (oldestMember) return oldestMember.userId;
|
||||
|
||||
// if there are no joined members other than us, use the oldest member
|
||||
room.currentState.getMembers().forEach((member) => {
|
||||
if (member.userId === myUserId) return;
|
||||
|
||||
if (typeof oldestMemberTs === 'undefined' || (member.events.member && member.events.member.getTs() < oldestMemberTs)) {
|
||||
oldestMember = member;
|
||||
oldestMemberTs = member.events.member.getTs();
|
||||
}
|
||||
});
|
||||
|
||||
if (typeof oldestMember === 'undefined') return myUserId;
|
||||
return oldestMember.userId;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} roomId
|
||||
* @param {boolean} isDM
|
||||
*/
|
||||
function join(roomId, isDM) {
|
||||
const mx = initMatrix.matrixClient;
|
||||
mx.joinRoom(roomId)
|
||||
.then(async () => {
|
||||
if (isDM) {
|
||||
const targetUserId = guessDMRoomTargetId(mx.getRoom(roomId), mx.getUserId());
|
||||
await addRoomToMDirect(roomId, targetUserId);
|
||||
}
|
||||
appDispatcher.dispatch({
|
||||
type: cons.actions.room.JOIN,
|
||||
roomId,
|
||||
isDM,
|
||||
});
|
||||
}).catch();
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} roomId
|
||||
* @param {boolean} isDM
|
||||
*/
|
||||
function leave(roomId, isDM) {
|
||||
const mx = initMatrix.matrixClient;
|
||||
mx.leave(roomId)
|
||||
.then(() => {
|
||||
appDispatcher.dispatch({
|
||||
type: cons.actions.room.LEAVE,
|
||||
roomId,
|
||||
isDM,
|
||||
});
|
||||
}).catch();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a room.
|
||||
* @param {Object} opts
|
||||
* @param {string} [opts.name]
|
||||
* @param {string} [opts.topic]
|
||||
* @param {boolean} [opts.isPublic=false] Sets room visibility to public
|
||||
* @param {string} [opts.roomAlias] Sets the room address
|
||||
* @param {boolean} [opts.isEncrypted=false] Makes room encrypted
|
||||
* @param {boolean} [opts.isDirect=false] Makes room as direct message
|
||||
* @param {string[]} [opts.invite=[]] An array of userId's to invite
|
||||
*/
|
||||
async function create(opts) {
|
||||
const mx = initMatrix.matrixClient;
|
||||
const options = {
|
||||
name: opts.name,
|
||||
topic: opts.topic,
|
||||
visibility: opts.isPublic === true ? 'public' : 'private',
|
||||
room_alias_name: opts.roomAlias,
|
||||
is_direct: opts.isDirect === true,
|
||||
invite: opts.invite || [],
|
||||
initial_state: [],
|
||||
};
|
||||
|
||||
if (opts.isPublic !== true && opts.isEncrypted === true) {
|
||||
options.initial_state.push({
|
||||
type: 'm.room.encryption',
|
||||
state_key: '',
|
||||
content: {
|
||||
algorithm: 'm.megolm.v1.aes-sha2',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await mx.createRoom(options);
|
||||
if (opts.isDirect === true && typeof opts.invite[0] !== 'undefined') {
|
||||
await addRoomToMDirect(result.room_id, opts.invite[0]);
|
||||
}
|
||||
appDispatcher.dispatch({
|
||||
type: cons.actions.room.CREATE,
|
||||
roomId: result.room_id,
|
||||
isDM: opts.isDirect === true,
|
||||
});
|
||||
return result;
|
||||
} catch (e) {
|
||||
const errcodes = ['M_UNKNOWN', 'M_BAD_JSON', 'M_ROOM_IN_USE', 'M_INVALID_ROOM_STATE', 'M_UNSUPPORTED_ROOM_VERSION'];
|
||||
if (errcodes.find((errcode) => errcode === e.errcode)) {
|
||||
appDispatcher.dispatch({
|
||||
type: cons.actions.room.error.CREATE,
|
||||
error: e,
|
||||
});
|
||||
throw new Error(e);
|
||||
}
|
||||
throw new Error('Something went wrong!');
|
||||
}
|
||||
}
|
||||
|
||||
async function invite(roomId, userId) {
|
||||
const mx = initMatrix.matrixClient;
|
||||
|
||||
try {
|
||||
const result = await mx.invite(roomId, userId);
|
||||
return result;
|
||||
} catch (e) {
|
||||
throw new Error(e);
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
join, leave, create, invite,
|
||||
};
|
||||
4
src/client/dispatcher.js
Normal file
4
src/client/dispatcher.js
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
import { Dispatcher } from 'flux';
|
||||
|
||||
const appDispatcher = new Dispatcher();
|
||||
export default appDispatcher;
|
||||
89
src/client/initMatrix.js
Normal file
89
src/client/initMatrix.js
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
import EventEmitter from 'events';
|
||||
import * as sdk from 'matrix-js-sdk';
|
||||
|
||||
import { secret } from './state/auth';
|
||||
import RoomList from './state/RoomList';
|
||||
import RoomsInput from './state/RoomsInput';
|
||||
|
||||
global.Olm = require('olm');
|
||||
|
||||
class InitMatrix extends EventEmitter {
|
||||
async init() {
|
||||
await this.startClient();
|
||||
this.setupSync();
|
||||
this.listenEvents();
|
||||
}
|
||||
|
||||
async startClient() {
|
||||
const indexedDBStore = new sdk.IndexedDBStore({
|
||||
indexedDB: global.indexedDB,
|
||||
localStorage: global.localStorage,
|
||||
dbName: 'web-sync-store',
|
||||
});
|
||||
await indexedDBStore.startup();
|
||||
|
||||
this.matrixClient = sdk.createClient({
|
||||
baseUrl: secret.baseUrl,
|
||||
accessToken: secret.accessToken,
|
||||
userId: secret.userId,
|
||||
store: indexedDBStore,
|
||||
sessionStore: new sdk.WebStorageSessionStore(global.localStorage),
|
||||
cryptoStore: new sdk.IndexedDBCryptoStore(global.indexedDB, 'crypto-store'),
|
||||
deviceId: secret.deviceId,
|
||||
});
|
||||
|
||||
await this.matrixClient.initCrypto();
|
||||
|
||||
await this.matrixClient.startClient({
|
||||
lazyLoadMembers: true,
|
||||
});
|
||||
this.matrixClient.setGlobalErrorOnUnknownDevices(false);
|
||||
}
|
||||
|
||||
setupSync() {
|
||||
const sync = {
|
||||
NULL: () => {
|
||||
console.log('NULL state');
|
||||
},
|
||||
SYNCING: () => {
|
||||
console.log('SYNCING state');
|
||||
},
|
||||
PREPARED: (prevState) => {
|
||||
console.log('PREPARED state');
|
||||
console.log('previous state: ', prevState);
|
||||
// TODO: remove global.initMatrix at end
|
||||
global.initMatrix = this;
|
||||
if (prevState === null) {
|
||||
this.roomList = new RoomList(this.matrixClient);
|
||||
this.roomsInput = new RoomsInput(this.matrixClient);
|
||||
this.emit('init_loading_finished');
|
||||
}
|
||||
},
|
||||
RECONNECTING: () => {
|
||||
console.log('RECONNECTING state');
|
||||
},
|
||||
CATCHUP: () => {
|
||||
console.log('CATCHUP state');
|
||||
},
|
||||
ERROR: () => {
|
||||
console.log('ERROR state');
|
||||
},
|
||||
STOPPED: () => {
|
||||
console.log('STOPPED state');
|
||||
},
|
||||
};
|
||||
this.matrixClient.on('sync', (state, prevState) => sync[state](prevState));
|
||||
}
|
||||
|
||||
listenEvents() {
|
||||
this.matrixClient.on('Session.logged_out', () => {
|
||||
this.matrixClient.clearStores();
|
||||
window.localStorage.clear();
|
||||
window.location.reload();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const initMatrix = new InitMatrix();
|
||||
|
||||
export default initMatrix;
|
||||
288
src/client/state/RoomList.js
Normal file
288
src/client/state/RoomList.js
Normal file
|
|
@ -0,0 +1,288 @@
|
|||
import EventEmitter from 'events';
|
||||
import appDispatcher from '../dispatcher';
|
||||
import cons from './cons';
|
||||
|
||||
class RoomList extends EventEmitter {
|
||||
constructor(matrixClient) {
|
||||
super();
|
||||
this.matrixClient = matrixClient;
|
||||
this.mDirects = this.getMDirects();
|
||||
|
||||
this.inviteDirects = new Set();
|
||||
this.inviteSpaces = new Set();
|
||||
this.inviteRooms = new Set();
|
||||
|
||||
this.directs = new Set();
|
||||
this.spaces = new Set();
|
||||
this.rooms = new Set();
|
||||
|
||||
this.processingRooms = new Map();
|
||||
|
||||
this._populateRooms();
|
||||
this._listenEvents();
|
||||
|
||||
appDispatcher.register(this.roomActions.bind(this));
|
||||
}
|
||||
|
||||
roomActions(action) {
|
||||
const addRoom = (roomId, isDM) => {
|
||||
const myRoom = this.matrixClient.getRoom(roomId);
|
||||
if (myRoom === null) return false;
|
||||
|
||||
if (isDM) this.directs.add(roomId);
|
||||
else if (myRoom.isSpaceRoom()) this.spaces.add(roomId);
|
||||
else this.rooms.add(roomId);
|
||||
return true;
|
||||
};
|
||||
const actions = {
|
||||
[cons.actions.room.JOIN]: () => {
|
||||
if (addRoom(action.roomId, action.isDM)) {
|
||||
setTimeout(() => {
|
||||
this.emit(cons.events.roomList.ROOM_JOINED, action.roomId);
|
||||
this.emit(cons.events.roomList.ROOMLIST_UPDATED);
|
||||
}, 100);
|
||||
} else {
|
||||
this.processingRooms.set(action.roomId, {
|
||||
roomId: action.roomId,
|
||||
isDM: action.isDM,
|
||||
task: 'JOIN',
|
||||
});
|
||||
}
|
||||
},
|
||||
[cons.actions.room.CREATE]: () => {
|
||||
if (addRoom(action.roomId, action.isDM)) {
|
||||
setTimeout(() => {
|
||||
this.emit(cons.events.roomList.ROOM_CREATED, action.roomId);
|
||||
this.emit(cons.events.roomList.ROOM_JOINED, action.roomId);
|
||||
this.emit(cons.events.roomList.ROOMLIST_UPDATED);
|
||||
}, 100);
|
||||
} else {
|
||||
this.processingRooms.set(action.roomId, {
|
||||
roomId: action.roomId,
|
||||
isDM: action.isDM,
|
||||
task: 'CREATE',
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
actions[action.type]?.();
|
||||
}
|
||||
|
||||
getMDirects() {
|
||||
const mDirectsId = new Set();
|
||||
const mDirect = this.matrixClient
|
||||
.getAccountData('m.direct')
|
||||
?.getContent();
|
||||
|
||||
if (typeof mDirect === 'undefined') return mDirectsId;
|
||||
|
||||
Object.keys(mDirect).forEach((direct) => {
|
||||
mDirect[direct].forEach((directId) => mDirectsId.add(directId));
|
||||
});
|
||||
|
||||
return mDirectsId;
|
||||
}
|
||||
|
||||
_populateRooms() {
|
||||
this.directs.clear();
|
||||
this.spaces.clear();
|
||||
this.rooms.clear();
|
||||
this.inviteDirects.clear();
|
||||
this.inviteSpaces.clear();
|
||||
this.inviteRooms.clear();
|
||||
this.matrixClient.getRooms().forEach((room) => {
|
||||
const { roomId } = room;
|
||||
const tombstone = room.currentState.events.get('m.room.tombstone');
|
||||
if (typeof tombstone !== 'undefined') {
|
||||
const repRoomId = tombstone.get('').getContent().replacement_room;
|
||||
const repRoomMembership = this.matrixClient.getRoom(repRoomId)?.getMyMembership();
|
||||
if (repRoomMembership === 'join') return;
|
||||
}
|
||||
|
||||
if (room.getMyMembership() === 'invite') {
|
||||
if (this._isDMInvite(room)) this.inviteDirects.add(roomId);
|
||||
else if (room.isSpaceRoom()) this.inviteSpaces.add(roomId);
|
||||
else this.inviteRooms.add(roomId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (room.getMyMembership() !== 'join') return;
|
||||
|
||||
if (this.mDirects.has(roomId)) this.directs.add(roomId);
|
||||
else if (room.isSpaceRoom()) this.spaces.add(roomId);
|
||||
else this.rooms.add(roomId);
|
||||
});
|
||||
}
|
||||
|
||||
_isDMInvite(room) {
|
||||
const me = room.getMember(this.matrixClient.getUserId());
|
||||
const myEventContent = me.events.member.getContent();
|
||||
return myEventContent.membership === 'invite' && myEventContent.is_direct;
|
||||
}
|
||||
|
||||
_listenEvents() {
|
||||
// Update roomList when m.direct changes
|
||||
this.matrixClient.on('accountData', (event) => {
|
||||
if (event.getType() !== 'm.direct') return;
|
||||
|
||||
const latestMDirects = this.getMDirects();
|
||||
|
||||
latestMDirects.forEach((directId) => {
|
||||
const myRoom = this.matrixClient.getRoom(directId);
|
||||
if (this.mDirects.has(directId)) return;
|
||||
|
||||
// Update mDirects
|
||||
this.mDirects.add(directId);
|
||||
|
||||
if (myRoom === null) return;
|
||||
|
||||
if (this._isDMInvite(myRoom)) return;
|
||||
|
||||
if (myRoom.getMyMembership === 'join' && !this.directs.has(directId)) {
|
||||
this.directs.add(directId);
|
||||
}
|
||||
|
||||
// Newly added room.
|
||||
// at this time my membership can be invite | join
|
||||
if (myRoom.getMyMembership() === 'join' && this.rooms.has(directId)) {
|
||||
// found a DM which accidentally gets added to this.rooms
|
||||
this.rooms.delete(directId);
|
||||
this.emit(cons.events.roomList.ROOMLIST_UPDATED);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this.matrixClient.on('Room.name', () => {
|
||||
this.emit(cons.events.roomList.ROOMLIST_UPDATED);
|
||||
});
|
||||
this.matrixClient.on('Room.receipt', (event) => {
|
||||
if (event.getType() === 'm.receipt') {
|
||||
const evContent = event.getContent();
|
||||
const userId = Object.keys(evContent[Object.keys(evContent)[0]]['m.read'])[0];
|
||||
if (userId !== this.matrixClient.getUserId()) return;
|
||||
this.emit(cons.events.roomList.ROOMLIST_UPDATED);
|
||||
}
|
||||
});
|
||||
|
||||
this.matrixClient.on('RoomState.events', (event) => {
|
||||
if (event.getType() !== 'm.room.join_rules') return;
|
||||
|
||||
this.emit(cons.events.roomList.ROOMLIST_UPDATED);
|
||||
});
|
||||
|
||||
this.matrixClient.on('Room.myMembership', (room, membership, prevMembership) => {
|
||||
// room => prevMembership = null | invite | join | leave | kick | ban | unban
|
||||
// room => membership = invite | join | leave | kick | ban | unban
|
||||
const { roomId } = room;
|
||||
|
||||
if (membership === 'unban') return;
|
||||
|
||||
// When user_reject/sender_undo room invite
|
||||
if (prevMembership === 'invite') {
|
||||
if (this.inviteDirects.has(roomId)) this.inviteDirects.delete(roomId);
|
||||
else if (this.inviteSpaces.has(roomId)) this.inviteSpaces.delete(roomId);
|
||||
else this.inviteRooms.delete(roomId);
|
||||
|
||||
this.emit(cons.events.roomList.INVITELIST_UPDATED, roomId);
|
||||
}
|
||||
|
||||
// When user get invited
|
||||
if (membership === 'invite') {
|
||||
if (this._isDMInvite(room)) this.inviteDirects.add(roomId);
|
||||
else if (room.isSpaceRoom()) this.inviteSpaces.add(roomId);
|
||||
else this.inviteRooms.add(roomId);
|
||||
|
||||
this.emit(cons.events.roomList.INVITELIST_UPDATED, roomId);
|
||||
return;
|
||||
}
|
||||
|
||||
// When user join room (first time) or start DM.
|
||||
if ((prevMembership === null || prevMembership === 'invite') && membership === 'join') {
|
||||
// when user create room/DM OR accept room/dm invite from this client.
|
||||
// we will update this.rooms/this.directs with user action
|
||||
if (this.directs.has(roomId) || this.spaces.has(roomId) || this.rooms.has(roomId)) return;
|
||||
|
||||
if (this.processingRooms.has(roomId)) {
|
||||
const procRoomInfo = this.processingRooms.get(roomId);
|
||||
|
||||
if (procRoomInfo.isDM) this.directs.add(roomId);
|
||||
else if (room.isSpaceRoom()) this.spaces.add(roomId);
|
||||
else this.rooms.add(roomId);
|
||||
|
||||
if (procRoomInfo.task === 'CREATE') this.emit(cons.events.roomList.ROOM_CREATED, roomId);
|
||||
this.emit(cons.events.roomList.ROOM_JOINED, roomId);
|
||||
this.emit(cons.events.roomList.ROOMLIST_UPDATED);
|
||||
|
||||
this.processingRooms.delete(roomId);
|
||||
return;
|
||||
}
|
||||
if (room.isSpaceRoom()) {
|
||||
this.spaces.add(roomId);
|
||||
|
||||
this.emit(cons.events.roomList.ROOM_JOINED, roomId);
|
||||
this.emit(cons.events.roomList.ROOMLIST_UPDATED);
|
||||
return;
|
||||
}
|
||||
|
||||
// below code intented to work when user create room/DM
|
||||
// OR accept room/dm invite from other client.
|
||||
// and we have to update our client. (it's ok to have 10sec delay)
|
||||
|
||||
// create a buffer of 10sec and HOPE client.accoundData get updated
|
||||
// then accoundData event listener will update this.mDirects.
|
||||
// and we will be able to know if it's a DM.
|
||||
// ----------
|
||||
// less likely situation:
|
||||
// if we don't get accountData with 10sec then:
|
||||
// we will temporary add it to this.rooms.
|
||||
// and in future when accountData get updated
|
||||
// accountData listener will automatically goona REMOVE it from this.rooms
|
||||
// and will ADD it to this.directs
|
||||
// and emit the cons.events.roomList.ROOMLIST_UPDATED to update the UI.
|
||||
|
||||
setTimeout(() => {
|
||||
if (this.directs.has(roomId) || this.spaces.has(roomId) || this.rooms.has(roomId)) return;
|
||||
if (this.mDirects.has(roomId)) this.directs.add(roomId);
|
||||
else this.rooms.add(roomId);
|
||||
|
||||
this.emit(cons.events.roomList.ROOM_JOINED, roomId);
|
||||
this.emit(cons.events.roomList.ROOMLIST_UPDATED);
|
||||
}, 10000);
|
||||
return;
|
||||
}
|
||||
|
||||
// when room is a DM add/remove it from DM's and return.
|
||||
if (this.directs.has(roomId)) {
|
||||
if (membership === 'leave' || membership === 'kick' || membership === 'ban') {
|
||||
this.directs.delete(roomId);
|
||||
this.emit(cons.events.roomList.ROOM_LEAVED, roomId);
|
||||
}
|
||||
}
|
||||
if (this.mDirects.has(roomId)) {
|
||||
if (membership === 'join') {
|
||||
this.directs.add(roomId);
|
||||
this.emit(cons.events.roomList.ROOM_JOINED, roomId);
|
||||
}
|
||||
this.emit(cons.events.roomList.ROOMLIST_UPDATED);
|
||||
return;
|
||||
}
|
||||
// when room is not a DM add/remove it from rooms.
|
||||
if (membership === 'leave' || membership === 'kick' || membership === 'ban') {
|
||||
if (room.isSpaceRoom()) this.spaces.delete(roomId);
|
||||
else this.rooms.delete(roomId);
|
||||
this.emit(cons.events.roomList.ROOM_LEAVED, roomId);
|
||||
}
|
||||
if (membership === 'join') {
|
||||
if (room.isSpaceRoom()) this.spaces.add(roomId);
|
||||
else this.rooms.add(roomId);
|
||||
this.emit(cons.events.roomList.ROOM_JOINED, roomId);
|
||||
}
|
||||
this.emit(cons.events.roomList.ROOMLIST_UPDATED);
|
||||
});
|
||||
|
||||
this.matrixClient.on('Room.timeline', () => {
|
||||
this.emit(cons.events.roomList.ROOMLIST_UPDATED);
|
||||
});
|
||||
}
|
||||
}
|
||||
export default RoomList;
|
||||
161
src/client/state/RoomTimeline.js
Normal file
161
src/client/state/RoomTimeline.js
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
import EventEmitter from 'events';
|
||||
import initMatrix from '../initMatrix';
|
||||
import cons from './cons';
|
||||
|
||||
class RoomTimeline extends EventEmitter {
|
||||
constructor(roomId) {
|
||||
super();
|
||||
this.matrixClient = initMatrix.matrixClient;
|
||||
this.roomId = roomId;
|
||||
this.room = this.matrixClient.getRoom(roomId);
|
||||
this.timeline = this.room.timeline;
|
||||
this.editedTimeline = this.getEditedTimeline();
|
||||
this.reactionTimeline = this.getReactionTimeline();
|
||||
this.isOngoingPagination = false;
|
||||
this.ongoingDecryptionCount = 0;
|
||||
this.typingMembers = new Set();
|
||||
|
||||
this._listenRoomTimeline = (event, room) => {
|
||||
if (room.roomId !== this.roomId) return;
|
||||
|
||||
if (event.isEncrypted()) {
|
||||
this.ongoingDecryptionCount += 1;
|
||||
return;
|
||||
}
|
||||
|
||||
this.timeline = this.room.timeline;
|
||||
if (this.isEdited(event)) {
|
||||
this.addToMap(this.editedTimeline, event);
|
||||
}
|
||||
if (this.isReaction(event)) {
|
||||
this.addToMap(this.reactionTimeline, event);
|
||||
}
|
||||
|
||||
if (this.ongoingDecryptionCount !== 0) return;
|
||||
this.emit(cons.events.roomTimeline.EVENT);
|
||||
};
|
||||
|
||||
this._listenDecryptEvent = (event) => {
|
||||
if (event.getRoomId() !== this.roomId) return;
|
||||
|
||||
if (this.ongoingDecryptionCount > 0) this.ongoingDecryptionCount -= 1;
|
||||
this.timeline = this.room.timeline;
|
||||
|
||||
if (this.ongoingDecryptionCount !== 0) return;
|
||||
this.emit(cons.events.roomTimeline.EVENT);
|
||||
};
|
||||
|
||||
this._listenTypingEvent = (event, member) => {
|
||||
if (member.roomId !== this.roomId) return;
|
||||
|
||||
const isTyping = member.typing;
|
||||
if (isTyping) this.typingMembers.add(member.userId);
|
||||
else this.typingMembers.delete(member.userId);
|
||||
this.emit(cons.events.roomTimeline.TYPING_MEMBERS_UPDATED, new Set([...this.typingMembers]));
|
||||
};
|
||||
this._listenReciptEvent = (event, room) => {
|
||||
if (room.roomId !== this.roomId) return;
|
||||
const receiptContent = event.getContent();
|
||||
if (this.timeline.length === 0) return;
|
||||
const tmlLastEvent = this.timeline[this.timeline.length - 1];
|
||||
const lastEventId = tmlLastEvent.getId();
|
||||
const lastEventRecipt = receiptContent[lastEventId];
|
||||
if (typeof lastEventRecipt === 'undefined') return;
|
||||
if (lastEventRecipt['m.read']) {
|
||||
this.emit(cons.events.roomTimeline.READ_RECEIPT);
|
||||
}
|
||||
};
|
||||
|
||||
this.matrixClient.on('Room.timeline', this._listenRoomTimeline);
|
||||
this.matrixClient.on('Event.decrypted', this._listenDecryptEvent);
|
||||
this.matrixClient.on('RoomMember.typing', this._listenTypingEvent);
|
||||
this.matrixClient.on('Room.receipt', this._listenReciptEvent);
|
||||
|
||||
// TODO: remove below line when release
|
||||
window.selectedRoom = this;
|
||||
|
||||
if (this.isEncryptedRoom()) this.room.decryptAllEvents();
|
||||
}
|
||||
|
||||
isEncryptedRoom() {
|
||||
return this.matrixClient.isRoomEncrypted(this.roomId);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
isEdited(mEvent) {
|
||||
return mEvent.getRelation()?.rel_type === 'm.replace';
|
||||
}
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
getRelateToId(mEvent) {
|
||||
const relation = mEvent.getRelation();
|
||||
return relation && relation.event_id;
|
||||
}
|
||||
|
||||
addToMap(myMap, mEvent) {
|
||||
const relateToId = this.getRelateToId(mEvent);
|
||||
if (relateToId === null) return null;
|
||||
|
||||
if (typeof myMap.get(relateToId) === 'undefined') myMap.set(relateToId, []);
|
||||
myMap.get(relateToId).push(mEvent);
|
||||
return mEvent;
|
||||
}
|
||||
|
||||
getEditedTimeline() {
|
||||
const mReplace = new Map();
|
||||
this.timeline.forEach((mEvent) => {
|
||||
if (this.isEdited(mEvent)) {
|
||||
this.addToMap(mReplace, mEvent);
|
||||
}
|
||||
});
|
||||
|
||||
return mReplace;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
isReaction(mEvent) {
|
||||
return mEvent.getType() === 'm.reaction';
|
||||
}
|
||||
|
||||
getReactionTimeline() {
|
||||
const mReaction = new Map();
|
||||
this.timeline.forEach((mEvent) => {
|
||||
if (this.isReaction(mEvent)) {
|
||||
this.addToMap(mReaction, mEvent);
|
||||
}
|
||||
});
|
||||
|
||||
return mReaction;
|
||||
}
|
||||
|
||||
paginateBack() {
|
||||
if (this.isOngoingPagination) return;
|
||||
this.isOngoingPagination = true;
|
||||
|
||||
const MSG_LIMIT = 30;
|
||||
this.matrixClient.scrollback(this.room, MSG_LIMIT).then(async (room) => {
|
||||
if (room.oldState.paginationToken === null) {
|
||||
// We have reached start of the timeline
|
||||
this.isOngoingPagination = false;
|
||||
if (this.isEncryptedRoom()) await this.room.decryptAllEvents();
|
||||
this.emit(cons.events.roomTimeline.PAGINATED, false);
|
||||
return;
|
||||
}
|
||||
this.editedTimeline = this.getEditedTimeline();
|
||||
this.reactionTimeline = this.getReactionTimeline();
|
||||
|
||||
this.isOngoingPagination = false;
|
||||
if (this.isEncryptedRoom()) await this.room.decryptAllEvents();
|
||||
this.emit(cons.events.roomTimeline.PAGINATED, true);
|
||||
});
|
||||
}
|
||||
|
||||
removeInternalListeners() {
|
||||
this.matrixClient.removeListener('Room.timeline', this._listenRoomTimeline);
|
||||
this.matrixClient.removeListener('Event.decrypted', this._listenDecryptEvent);
|
||||
this.matrixClient.removeListener('RoomMember.typing', this._listenTypingEvent);
|
||||
this.matrixClient.removeListener('Room.receipt', this._listenReciptEvent);
|
||||
}
|
||||
}
|
||||
|
||||
export default RoomTimeline;
|
||||
276
src/client/state/RoomsInput.js
Normal file
276
src/client/state/RoomsInput.js
Normal file
|
|
@ -0,0 +1,276 @@
|
|||
import EventEmitter from 'events';
|
||||
import encrypt from 'browser-encrypt-attachment';
|
||||
import cons from './cons';
|
||||
|
||||
function getImageDimension(file) {
|
||||
return new Promise((resolve) => {
|
||||
const img = new Image();
|
||||
img.onload = async () => {
|
||||
resolve({
|
||||
w: img.width,
|
||||
h: img.height,
|
||||
});
|
||||
};
|
||||
img.src = URL.createObjectURL(file);
|
||||
});
|
||||
}
|
||||
function loadVideo(videoFile) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const video = document.createElement('video');
|
||||
video.preload = 'metadata';
|
||||
video.playsInline = true;
|
||||
video.muted = true;
|
||||
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = (ev) => {
|
||||
// Wait until we have enough data to thumbnail the first frame.
|
||||
video.onloadeddata = async () => {
|
||||
resolve(video);
|
||||
video.pause();
|
||||
};
|
||||
video.onerror = (e) => {
|
||||
reject(e);
|
||||
};
|
||||
|
||||
video.src = ev.target.result;
|
||||
video.load();
|
||||
video.play();
|
||||
};
|
||||
reader.onerror = (e) => {
|
||||
reject(e);
|
||||
};
|
||||
reader.readAsDataURL(videoFile);
|
||||
});
|
||||
}
|
||||
function getVideoThumbnail(video, width, height, mimeType) {
|
||||
return new Promise((resolve) => {
|
||||
const MAX_WIDTH = 800;
|
||||
const MAX_HEIGHT = 600;
|
||||
let targetWidth = width;
|
||||
let targetHeight = height;
|
||||
if (targetHeight > MAX_HEIGHT) {
|
||||
targetWidth = Math.floor(targetWidth * (MAX_HEIGHT / targetHeight));
|
||||
targetHeight = MAX_HEIGHT;
|
||||
}
|
||||
if (targetWidth > MAX_WIDTH) {
|
||||
targetHeight = Math.floor(targetHeight * (MAX_WIDTH / targetWidth));
|
||||
targetWidth = MAX_WIDTH;
|
||||
}
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = targetWidth;
|
||||
canvas.height = targetHeight;
|
||||
const context = canvas.getContext('2d');
|
||||
context.drawImage(video, 0, 0, targetWidth, targetHeight);
|
||||
|
||||
canvas.toBlob((thumbnail) => {
|
||||
resolve({
|
||||
thumbnail,
|
||||
info: {
|
||||
w: targetWidth,
|
||||
h: targetHeight,
|
||||
mimetype: thumbnail.type,
|
||||
size: thumbnail.size,
|
||||
},
|
||||
});
|
||||
}, mimeType);
|
||||
});
|
||||
}
|
||||
|
||||
class RoomsInput extends EventEmitter {
|
||||
constructor(mx) {
|
||||
super();
|
||||
|
||||
this.matrixClient = mx;
|
||||
this.roomIdToInput = new Map();
|
||||
}
|
||||
|
||||
cleanEmptyEntry(roomId) {
|
||||
const input = this.getInput(roomId);
|
||||
const isEmpty = typeof input.attachment === 'undefined'
|
||||
&& (typeof input.message === 'undefined' || input.message === '');
|
||||
if (isEmpty) {
|
||||
this.roomIdToInput.delete(roomId);
|
||||
}
|
||||
}
|
||||
|
||||
getInput(roomId) {
|
||||
return this.roomIdToInput.get(roomId) || {};
|
||||
}
|
||||
|
||||
setMessage(roomId, message) {
|
||||
const input = this.getInput(roomId);
|
||||
input.message = message;
|
||||
this.roomIdToInput.set(roomId, input);
|
||||
if (message === '') this.cleanEmptyEntry(roomId);
|
||||
}
|
||||
|
||||
getMessage(roomId) {
|
||||
const input = this.getInput(roomId);
|
||||
if (typeof input.message === 'undefined') return '';
|
||||
return input.message;
|
||||
}
|
||||
|
||||
setAttachment(roomId, file) {
|
||||
const input = this.getInput(roomId);
|
||||
input.attachment = {
|
||||
file,
|
||||
};
|
||||
this.roomIdToInput.set(roomId, input);
|
||||
}
|
||||
|
||||
getAttachment(roomId) {
|
||||
const input = this.getInput(roomId);
|
||||
if (typeof input.attachment === 'undefined') return null;
|
||||
return input.attachment.file;
|
||||
}
|
||||
|
||||
cancelAttachment(roomId) {
|
||||
const input = this.getInput(roomId);
|
||||
if (typeof input.attachment === 'undefined') return;
|
||||
|
||||
const { uploadingPromise } = input.attachment;
|
||||
|
||||
if (uploadingPromise) {
|
||||
this.matrixClient.cancelUpload(uploadingPromise);
|
||||
delete input.attachment.uploadingPromise;
|
||||
}
|
||||
if (input.message) {
|
||||
delete input.attachment;
|
||||
delete input.isSending;
|
||||
this.roomIdToInput.set(roomId, input);
|
||||
} else {
|
||||
this.roomIdToInput.delete(roomId);
|
||||
}
|
||||
this.emit(cons.events.roomsInput.ATTACHMENT_CANCELED, roomId);
|
||||
}
|
||||
|
||||
isSending(roomId) {
|
||||
return this.roomIdToInput.get(roomId)?.isSending || false;
|
||||
}
|
||||
|
||||
async sendInput(roomId) {
|
||||
const input = this.getInput(roomId);
|
||||
input.isSending = true;
|
||||
this.roomIdToInput.set(roomId, input);
|
||||
if (input.attachment) {
|
||||
await this.sendFile(roomId, input.attachment.file);
|
||||
}
|
||||
|
||||
if (this.getMessage(roomId).trim() !== '') {
|
||||
const content = {
|
||||
body: input.message,
|
||||
msgtype: 'm.text',
|
||||
};
|
||||
this.matrixClient.sendMessage(roomId, content);
|
||||
}
|
||||
|
||||
if (this.isSending(roomId)) this.roomIdToInput.delete(roomId);
|
||||
this.emit(cons.events.roomsInput.MESSAGE_SENT, roomId);
|
||||
}
|
||||
|
||||
async sendFile(roomId, file) {
|
||||
const fileType = file.type.slice(0, file.type.indexOf('/'));
|
||||
const info = {
|
||||
mimetype: file.type,
|
||||
size: file.size,
|
||||
};
|
||||
const content = { info };
|
||||
let uploadData = null;
|
||||
|
||||
if (fileType === 'image') {
|
||||
const imgDimension = await getImageDimension(file);
|
||||
|
||||
info.w = imgDimension.w;
|
||||
info.h = imgDimension.h;
|
||||
|
||||
content.msgtype = 'm.image';
|
||||
content.body = file.name || 'Image';
|
||||
} else if (fileType === 'video') {
|
||||
content.msgtype = 'm.video';
|
||||
content.body = file.name || 'Video';
|
||||
|
||||
try {
|
||||
const video = await loadVideo(file);
|
||||
info.w = video.videoWidth;
|
||||
info.h = video.videoHeight;
|
||||
const thumbnailData = await getVideoThumbnail(video, video.videoWidth, video.videoHeight, 'image/jpeg');
|
||||
const thumbnailUploadData = await this.uploadFile(roomId, thumbnailData.thumbnail);
|
||||
info.thumbnail_info = thumbnailData.info;
|
||||
if (this.matrixClient.isRoomEncrypted(roomId)) {
|
||||
info.thumbnail_file = thumbnailUploadData.file;
|
||||
} else {
|
||||
info.thumbnail_url = thumbnailUploadData.url;
|
||||
}
|
||||
} catch (e) {
|
||||
this.emit(cons.events.roomsInput.FILE_UPLOAD_CANCELED, roomId);
|
||||
return;
|
||||
}
|
||||
} else if (fileType === 'audio') {
|
||||
content.msgtype = 'm.audio';
|
||||
content.body = file.name || 'Audio';
|
||||
} else {
|
||||
content.msgtype = 'm.file';
|
||||
content.body = file.name || 'File';
|
||||
}
|
||||
|
||||
try {
|
||||
uploadData = await this.uploadFile(roomId, file, (data) => {
|
||||
// data have two properties: data.loaded, data.total
|
||||
this.emit(cons.events.roomsInput.UPLOAD_PROGRESS_CHANGES, roomId, data);
|
||||
});
|
||||
this.emit(cons.events.roomsInput.FILE_UPLOADED, roomId);
|
||||
} catch (e) {
|
||||
this.emit(cons.events.roomsInput.FILE_UPLOAD_CANCELED, roomId);
|
||||
return;
|
||||
}
|
||||
if (this.matrixClient.isRoomEncrypted(roomId)) {
|
||||
content.file = uploadData.file;
|
||||
await this.matrixClient.sendMessage(roomId, content);
|
||||
} else {
|
||||
content.url = uploadData.url;
|
||||
await this.matrixClient.sendMessage(roomId, content);
|
||||
}
|
||||
}
|
||||
|
||||
async uploadFile(roomId, file, progressHandler) {
|
||||
const isEncryptedRoom = this.matrixClient.isRoomEncrypted(roomId);
|
||||
|
||||
let encryptInfo = null;
|
||||
let encryptBlob = null;
|
||||
|
||||
if (isEncryptedRoom) {
|
||||
const dataBuffer = await file.arrayBuffer();
|
||||
if (typeof this.getInput(roomId).attachment === 'undefined') throw new Error('Attachment canceled');
|
||||
const encryptedResult = await encrypt.encryptAttachment(dataBuffer);
|
||||
if (typeof this.getInput(roomId).attachment === 'undefined') throw new Error('Attachment canceled');
|
||||
encryptInfo = encryptedResult.info;
|
||||
encryptBlob = new Blob([encryptedResult.data]);
|
||||
}
|
||||
|
||||
const uploadingPromise = this.matrixClient.uploadContent(isEncryptedRoom ? encryptBlob : file, {
|
||||
// don't send filename if room is encrypted.
|
||||
includeFilename: !isEncryptedRoom,
|
||||
progressHandler,
|
||||
});
|
||||
|
||||
const input = this.getInput(roomId);
|
||||
input.attachment.uploadingPromise = uploadingPromise;
|
||||
this.roomIdToInput.set(roomId, input);
|
||||
|
||||
const url = await uploadingPromise;
|
||||
|
||||
delete input.attachment.uploadingPromise;
|
||||
this.roomIdToInput.set(roomId, input);
|
||||
|
||||
if (isEncryptedRoom) {
|
||||
encryptInfo.url = url;
|
||||
if (file.type) encryptInfo.mimetype = file.type;
|
||||
return { file: encryptInfo };
|
||||
}
|
||||
return { url };
|
||||
}
|
||||
}
|
||||
|
||||
export default RoomsInput;
|
||||
19
src/client/state/auth.js
Normal file
19
src/client/state/auth.js
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import cons from './cons';
|
||||
|
||||
function getSecret(key) {
|
||||
return localStorage.getItem(key);
|
||||
}
|
||||
|
||||
const isAuthanticated = () => getSecret(cons.secretKey.ACCESS_TOKEN) !== null;
|
||||
|
||||
const secret = {
|
||||
accessToken: getSecret(cons.secretKey.ACCESS_TOKEN),
|
||||
deviceId: getSecret(cons.secretKey.DEVICE_ID),
|
||||
userId: getSecret(cons.secretKey.USER_ID),
|
||||
baseUrl: getSecret(cons.secretKey.BASE_URL),
|
||||
};
|
||||
|
||||
export {
|
||||
isAuthanticated,
|
||||
secret,
|
||||
};
|
||||
65
src/client/state/cons.js
Normal file
65
src/client/state/cons.js
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
const cons = {
|
||||
secretKey: {
|
||||
ACCESS_TOKEN: 'cinny_access_token',
|
||||
DEVICE_ID: 'cinny_device_id',
|
||||
USER_ID: 'cinny_user_id',
|
||||
BASE_URL: 'cinny_hs_base_url',
|
||||
},
|
||||
DEVICE_DISPLAY_NAME: 'Cinny Web',
|
||||
actions: {
|
||||
navigation: {
|
||||
CHANGE_TAB: 'CHANGE_TAB',
|
||||
SELECT_ROOM: 'SELECT_ROOM',
|
||||
TOGGLE_PEOPLE_DRAWER: 'TOGGLE_PEOPLE_DRAWER',
|
||||
OPEN_INVITE_LIST: 'OPEN_INVITE_LIST',
|
||||
OPEN_PUBLIC_CHANNELS: 'OPEN_PUBLIC_CHANNELS',
|
||||
OPEN_CREATE_CHANNEL: 'OPEN_CREATE_CHANNEL',
|
||||
OPEN_INVITE_USER: 'OPEN_INVITE_USER',
|
||||
OPEN_SETTINGS: 'OPEN_SETTINGS',
|
||||
},
|
||||
room: {
|
||||
JOIN: 'JOIN',
|
||||
LEAVE: 'LEAVE',
|
||||
CREATE: 'CREATE',
|
||||
error: {
|
||||
CREATE: 'CREATE',
|
||||
},
|
||||
},
|
||||
},
|
||||
events: {
|
||||
navigation: {
|
||||
TAB_CHANGED: 'TAB_CHANGED',
|
||||
ROOM_SELECTED: 'ROOM_SELECTED',
|
||||
PEOPLE_DRAWER_TOGGLED: 'PEOPLE_DRAWER_TOGGLED',
|
||||
INVITE_LIST_OPENED: 'INVITE_LIST_OPENED',
|
||||
PUBLIC_CHANNELS_OPENED: 'PUBLIC_CHANNELS_OPENED',
|
||||
CREATE_CHANNEL_OPENED: 'CREATE_CHANNEL_OPENED',
|
||||
INVITE_USER_OPENED: 'INVITE_USER_OPENED',
|
||||
SETTINGS_OPENED: 'SETTINGS_OPENED',
|
||||
},
|
||||
roomList: {
|
||||
ROOMLIST_UPDATED: 'ROOMLIST_UPDATED',
|
||||
INVITELIST_UPDATED: 'INVITELIST_UPDATED',
|
||||
ROOM_JOINED: 'ROOM_JOINED',
|
||||
ROOM_LEAVED: 'ROOM_LEAVED',
|
||||
ROOM_CREATED: 'ROOM_CREATED',
|
||||
},
|
||||
roomTimeline: {
|
||||
EVENT: 'EVENT',
|
||||
PAGINATED: 'PAGINATED',
|
||||
TYPING_MEMBERS_UPDATED: 'TYPING_MEMBERS_UPDATED',
|
||||
READ_RECEIPT: 'READ_RECEIPT',
|
||||
},
|
||||
roomsInput: {
|
||||
MESSAGE_SENT: 'MESSAGE_SENT',
|
||||
FILE_UPLOADED: 'FILE_UPLOADED',
|
||||
UPLOAD_PROGRESS_CHANGES: 'UPLOAD_PROGRESS_CHANGES',
|
||||
FILE_UPLOAD_CANCELED: 'FILE_UPLOAD_CANCELED',
|
||||
ATTACHMENT_CANCELED: 'ATTACHMENT_CANCELED',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
Object.freeze(cons);
|
||||
|
||||
export default cons;
|
||||
59
src/client/state/navigation.js
Normal file
59
src/client/state/navigation.js
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import EventEmitter from 'events';
|
||||
import appDispatcher from '../dispatcher';
|
||||
import cons from './cons';
|
||||
|
||||
class Navigation extends EventEmitter {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.activeTab = 'channels';
|
||||
this.selectedRoom = null;
|
||||
this.isPeopleDrawerVisible = true;
|
||||
}
|
||||
|
||||
getActiveTab() {
|
||||
return this.activeTab;
|
||||
}
|
||||
|
||||
getActiveRoom() {
|
||||
return this.selectedRoom;
|
||||
}
|
||||
|
||||
navigate(action) {
|
||||
const actions = {
|
||||
[cons.actions.navigation.CHANGE_TAB]: () => {
|
||||
this.activeTab = action.tabId;
|
||||
this.emit(cons.events.navigation.TAB_CHANGED, this.activeTab);
|
||||
},
|
||||
[cons.actions.navigation.SELECT_ROOM]: () => {
|
||||
this.selectedRoom = action.roomId;
|
||||
this.emit(cons.events.navigation.ROOM_SELECTED, this.selectedRoom);
|
||||
},
|
||||
[cons.actions.navigation.TOGGLE_PEOPLE_DRAWER]: () => {
|
||||
this.isPeopleDrawerVisible = !this.isPeopleDrawerVisible;
|
||||
this.emit(cons.events.navigation.PEOPLE_DRAWER_TOGGLED, this.isPeopleDrawerVisible);
|
||||
},
|
||||
[cons.actions.navigation.OPEN_INVITE_LIST]: () => {
|
||||
this.emit(cons.events.navigation.INVITE_LIST_OPENED);
|
||||
},
|
||||
[cons.actions.navigation.OPEN_PUBLIC_CHANNELS]: () => {
|
||||
this.emit(cons.events.navigation.PUBLIC_CHANNELS_OPENED);
|
||||
},
|
||||
[cons.actions.navigation.OPEN_CREATE_CHANNEL]: () => {
|
||||
this.emit(cons.events.navigation.CREATE_CHANNEL_OPENED);
|
||||
},
|
||||
[cons.actions.navigation.OPEN_INVITE_USER]: () => {
|
||||
this.emit(cons.events.navigation.INVITE_USER_OPENED, action.roomId);
|
||||
},
|
||||
[cons.actions.navigation.OPEN_SETTINGS]: () => {
|
||||
this.emit(cons.events.navigation.SETTINGS_OPENED);
|
||||
},
|
||||
};
|
||||
actions[action.type]?.();
|
||||
}
|
||||
}
|
||||
|
||||
const navigation = new Navigation();
|
||||
appDispatcher.register(navigation.navigate.bind(navigation));
|
||||
|
||||
export default navigation;
|
||||
36
src/client/state/settings.js
Normal file
36
src/client/state/settings.js
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
class Settings {
|
||||
constructor() {
|
||||
this.themes = ['', 'silver-theme', 'dark-theme', 'butter-theme'];
|
||||
this.themeIndex = this.getThemeIndex();
|
||||
}
|
||||
|
||||
getThemeIndex() {
|
||||
if (typeof this.themeIndex === 'number') return this.themeIndex;
|
||||
|
||||
let settings = localStorage.getItem('settings');
|
||||
if (settings === null) return 0;
|
||||
settings = JSON.parse(settings);
|
||||
if (typeof settings.themeIndex === 'undefined') return 0;
|
||||
// eslint-disable-next-line radix
|
||||
return parseInt(settings.themeIndex);
|
||||
}
|
||||
|
||||
getThemeName() {
|
||||
return this.themes[this.themeIndex];
|
||||
}
|
||||
|
||||
setTheme(themeIndex) {
|
||||
const appBody = document.getElementById('appBody');
|
||||
this.themes.forEach((themeName) => {
|
||||
if (themeName === '') return;
|
||||
appBody.classList.remove(themeName);
|
||||
});
|
||||
if (this.themes[themeIndex] !== '') appBody.classList.add(this.themes[themeIndex]);
|
||||
localStorage.setItem('settings', JSON.stringify({ themeIndex }));
|
||||
this.themeIndex = themeIndex;
|
||||
}
|
||||
}
|
||||
|
||||
const settings = new Settings();
|
||||
|
||||
export default settings;
|
||||
Loading…
Add table
Add a link
Reference in a new issue