Merge remote-tracking branch 'upstream/message-sending-refactor' into audric/refactor-message-sending
commit
ee6ee7ec4f
@ -1,3 +1,418 @@
|
|||||||
|
import { ConversationType } from '../../ts/state/ducks/conversations';
|
||||||
|
import { Mesasge } from '../../ts/types/Message';
|
||||||
|
|
||||||
|
type IdentityKey = {
|
||||||
|
id: string;
|
||||||
|
publicKey: ArrayBuffer;
|
||||||
|
firstUse: boolean;
|
||||||
|
verified: number;
|
||||||
|
nonblockingApproval: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type PreKey = {
|
||||||
|
id: number;
|
||||||
|
publicKey: ArrayBuffer;
|
||||||
|
privateKey: ArrayBuffer;
|
||||||
|
recipient: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type SignedPreKey = {
|
||||||
|
id: number;
|
||||||
|
publicKey: ArrayBuffer;
|
||||||
|
privateKey: ArrayBuffer;
|
||||||
|
created_at: number;
|
||||||
|
confirmed: boolean;
|
||||||
|
signature: ArrayBuffer;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ContactPreKey = {
|
||||||
|
id: number;
|
||||||
|
identityKeyString: string;
|
||||||
|
publicKey: ArrayBuffer;
|
||||||
|
keyId: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ContactSignedPreKey = {
|
||||||
|
id: number;
|
||||||
|
identityKeyString: string;
|
||||||
|
publicKey: ArrayBuffer;
|
||||||
|
keyId: number;
|
||||||
|
signature: ArrayBuffer;
|
||||||
|
created_at: number;
|
||||||
|
confirmed: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type PairingAuthorisation = {
|
||||||
|
primaryDevicePubKey: string;
|
||||||
|
secondaryDevicePubKey: string;
|
||||||
|
requestSignature: ArrayBuffer;
|
||||||
|
grantSignature: ArrayBuffer | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type GuardNode = {
|
||||||
|
ed25519PubKey: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type SwarmNode = {
|
||||||
|
address: string;
|
||||||
|
ip: string;
|
||||||
|
port: string;
|
||||||
|
pubkey_ed25519: string;
|
||||||
|
pubkey_x25519: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type StorageItem = {
|
||||||
|
id: string;
|
||||||
|
value: any;
|
||||||
|
};
|
||||||
|
|
||||||
|
type SessionDataInfo = {
|
||||||
|
id: string;
|
||||||
|
number: string;
|
||||||
|
deviceId: number;
|
||||||
|
record: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ServerToken = {
|
||||||
|
serverUrl: string;
|
||||||
|
token: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Basic
|
||||||
export function searchMessages(query: string): Promise<Array<any>>;
|
export function searchMessages(query: string): Promise<Array<any>>;
|
||||||
export function searchConversations(query: string): Promise<Array<any>>;
|
export function searchConversations(query: string): Promise<Array<any>>;
|
||||||
export function getPrimaryDeviceFor(pubKey: string): Promise<string | null>;
|
export function shutdown(): Promise<void>;
|
||||||
|
export function close(): Promise<void>;
|
||||||
|
export function removeDB(): Promise<void>;
|
||||||
|
export function removeIndexedDBFiles(): Promise<void>;
|
||||||
|
export function getPasswordHash(): Promise<string | null>;
|
||||||
|
|
||||||
|
// Identity Keys
|
||||||
|
export function createOrUpdateIdentityKey(data: IdentityKey): Promise<void>;
|
||||||
|
export function getIdentityKeyById(id: string): Promise<IdentityKey | null>;
|
||||||
|
export function bulkAddIdentityKeys(array: Array<IdentityKey>): Promise<void>;
|
||||||
|
export function removeIdentityKeyById(id: string): Promise<void>;
|
||||||
|
export function removeAllIdentityKeys(): Promise<void>;
|
||||||
|
|
||||||
|
// Pre Keys
|
||||||
|
export function createOrUpdatePreKey(data: PreKey): Promise<void>;
|
||||||
|
export function getPreKeyById(id: number): Promise<PreKey | null>;
|
||||||
|
export function getPreKeyByRecipient(recipient: string): Promise<PreKey | null>;
|
||||||
|
export function bulkAddPreKeys(data: Array<PreKey>): Promise<void>;
|
||||||
|
export function removePreKeyById(id: number): Promise<void>;
|
||||||
|
export function getAllPreKeys(): Promise<Array<PreKey>>;
|
||||||
|
|
||||||
|
// Signed Pre Keys
|
||||||
|
export function createOrUpdateSignedPreKey(data: SignedPreKey): Promise<void>;
|
||||||
|
export function getSignedPreKeyById(id: number): Promise<SignedPreKey | null>;
|
||||||
|
export function getAllSignedPreKeys(): Promise<SignedPreKey | null>;
|
||||||
|
export function bulkAddSignedPreKeys(array: Array<SignedPreKey>): Promise<void>;
|
||||||
|
export function removeSignedPreKeyById(id: number): Promise<void>;
|
||||||
|
export function removeAllSignedPreKeys(): Promise<void>;
|
||||||
|
|
||||||
|
// Contact Pre Key
|
||||||
|
export function createOrUpdateContactPreKey(data: ContactPreKey): Promise<void>;
|
||||||
|
export function getContactPreKeyById(id: number): Promise<ContactPreKey | null>;
|
||||||
|
export function getContactPreKeyByIdentityKey(
|
||||||
|
key: string
|
||||||
|
): Promise<ContactPreKey | null>;
|
||||||
|
export function getContactPreKeys(
|
||||||
|
keyId: number,
|
||||||
|
identityKeyString: string
|
||||||
|
): Promise<Array<ContactPreKey>>;
|
||||||
|
export function getAllContactPreKeys(): Promise<Array<ContactPreKey>>;
|
||||||
|
export function bulkAddContactPreKeys(
|
||||||
|
array: Array<ContactPreKey>
|
||||||
|
): Promise<void>;
|
||||||
|
export function removeContactPreKeyByIdentityKey(id: number): Promise<void>;
|
||||||
|
export function removeAllContactPreKeys(): Promise<void>;
|
||||||
|
|
||||||
|
// Contact Signed Pre Key
|
||||||
|
export function createOrUpdateContactSignedPreKey(
|
||||||
|
data: ContactSignedPreKey
|
||||||
|
): Promise<void>;
|
||||||
|
export function getContactSignedPreKeyById(
|
||||||
|
id: number
|
||||||
|
): Promise<ContactSignedPreKey | null>;
|
||||||
|
export function getContactSignedPreKeyByIdentityKey(
|
||||||
|
key: string
|
||||||
|
): Promise<ContactSignedPreKey | null>;
|
||||||
|
export function getContactSignedPreKeys(
|
||||||
|
keyId: number,
|
||||||
|
identityKeyString: string
|
||||||
|
): Promise<Array<ContactSignedPreKey>>;
|
||||||
|
export function bulkAddContactSignedPreKeys(
|
||||||
|
array: Array<ContactSignedPreKey>
|
||||||
|
): Promise<void>;
|
||||||
|
export function removeContactSignedPreKeyByIdentityKey(
|
||||||
|
id: string
|
||||||
|
): Promise<void>;
|
||||||
|
export function removeAllContactSignedPreKeys(): Promise<void>;
|
||||||
|
|
||||||
|
// Authorisations & Linking
|
||||||
|
export function createOrUpdatePairingAuthorisation(
|
||||||
|
data: PairingAuthorisation
|
||||||
|
): Promise<void>;
|
||||||
|
export function removePairingAuthorisationForSecondaryPubKey(
|
||||||
|
pubKey: string
|
||||||
|
): Promise<void>;
|
||||||
|
export function getGrantAuthorisationsForPrimaryPubKey(
|
||||||
|
pubKey: string
|
||||||
|
): Promise<Array<PairingAuthorisation>>;
|
||||||
|
export function getGrantAuthorisationForSecondaryPubKey(
|
||||||
|
pubKey: string
|
||||||
|
): Promise<PairingAuthorisation | null>;
|
||||||
|
export function getAuthorisationForSecondaryPubKey(
|
||||||
|
pubKey: string
|
||||||
|
): Promise<PairingAuthorisation | null>;
|
||||||
|
export function getSecondaryDevicesFor(
|
||||||
|
primaryDevicePubKey: string
|
||||||
|
): Promise<Array<string>>;
|
||||||
|
export function getPrimaryDeviceFor(
|
||||||
|
secondaryDevicePubKey: string
|
||||||
|
): Promise<string | null>;
|
||||||
|
export function getPairedDevicesFor(pubKey: string): Promise<Array<string>>;
|
||||||
|
|
||||||
|
// Guard Nodes
|
||||||
|
export function getGuardNodes(): Promise<GuardNode>;
|
||||||
|
export function updateGuardNodes(nodes: Array<string>): Promise<void>;
|
||||||
|
|
||||||
|
// Storage Items
|
||||||
|
export function createOrUpdateItem(data: StorageItem): Promise<void>;
|
||||||
|
export function getItemById(id: string): Promise<StorageItem>;
|
||||||
|
export function getAlItems(): Promise<Array<StorageItem>>;
|
||||||
|
export function bulkAddItems(array: Array<StorageItem>): Promise<void>;
|
||||||
|
export function removeItemById(id: string): Promise<void>;
|
||||||
|
export function removeAllItems(): Promise<void>;
|
||||||
|
|
||||||
|
// Sessions
|
||||||
|
export function createOrUpdateSession(data: SessionDataInfo): Promise<void>;
|
||||||
|
export function getAllSessions(): Promise<Array<SessionDataInfo>>;
|
||||||
|
export function getSessionById(id: string): Promise<SessionDataInfo>;
|
||||||
|
export function getSessionsByNumber(number: string): Promise<SessionDataInfo>;
|
||||||
|
export function bulkAddSessions(array: Array<SessionDataInfo>): Promise<void>;
|
||||||
|
export function removeSessionById(id: string): Promise<void>;
|
||||||
|
export function removeSessionsByNumber(number: string): Promise<void>;
|
||||||
|
export function removeAllSessions(): Promise<void>;
|
||||||
|
|
||||||
|
// Conversations
|
||||||
|
export function getConversationCount(): Promise<number>;
|
||||||
|
export function saveConversation(data: ConversationType): Promise<void>;
|
||||||
|
export function saveConversations(data: Array<ConversationType>): Promise<void>;
|
||||||
|
export function updateConversation(data: ConversationType): Promise<void>;
|
||||||
|
export function removeConversation(id: string): Promise<void>;
|
||||||
|
|
||||||
|
export function getAllConversations({
|
||||||
|
ConversationCollection,
|
||||||
|
}: {
|
||||||
|
ConversationCollection: any;
|
||||||
|
}): Promise<Array<ConversationCollection>>;
|
||||||
|
|
||||||
|
export function getAllConversationIds(): Promise<Array<string>>;
|
||||||
|
export function getAllPrivateConversations(): Promise<Array<string>>;
|
||||||
|
export function getAllPublicConversations(): Promise<Array<string>>;
|
||||||
|
export function getPublicConversationsByServer(
|
||||||
|
server: string,
|
||||||
|
{ ConversationCollection }: { ConversationCollection: any }
|
||||||
|
): Promise<ConversationCollection>;
|
||||||
|
export function getPubkeysInPublicConversation(
|
||||||
|
id: string
|
||||||
|
): Promise<Array<string>>;
|
||||||
|
export function savePublicServerToken(data: ServerToken): Promise<void>;
|
||||||
|
export function getPublicServerTokenByServerUrl(
|
||||||
|
serverUrl: string
|
||||||
|
): Promise<string>;
|
||||||
|
export function getAllGroupsInvolvingId(
|
||||||
|
id: string,
|
||||||
|
{ ConversationCollection }: { ConversationCollection: any }
|
||||||
|
): Promise<Array<ConversationCollection>>;
|
||||||
|
|
||||||
|
// Returns conversation row
|
||||||
|
// TODO: Make strict return types for search
|
||||||
|
export function searchConversations(query: string): Promise<any>;
|
||||||
|
export function searchMessages(query: string): Promise<any>;
|
||||||
|
export function searchMessagesInConversation(
|
||||||
|
query: string,
|
||||||
|
conversationId: string,
|
||||||
|
{ limit }?: { limit: any }
|
||||||
|
): Promise<any>;
|
||||||
|
export function getMessageCount(): Promise<number>;
|
||||||
|
export function saveMessage(
|
||||||
|
data: Mesasge,
|
||||||
|
{ forceSave, Message }?: { forceSave: any; Message: any }
|
||||||
|
): Promise<string>;
|
||||||
|
export function cleanSeenMessages(): Promise<void>;
|
||||||
|
export function cleanLastHashes(): Promise<void>;
|
||||||
|
export function saveSeenMessageHash(data: {
|
||||||
|
expiresAt: number;
|
||||||
|
hash: string;
|
||||||
|
}): Promise<void>;
|
||||||
|
|
||||||
|
// TODO: Strictly type the following
|
||||||
|
export function updateLastHash(data: any): Promise<any>;
|
||||||
|
export function saveSeenMessageHashes(data: any): Promise<any>;
|
||||||
|
export function saveLegacyMessage(data: any): Promise<any>;
|
||||||
|
export function saveMessages(
|
||||||
|
arrayOfMessages: any,
|
||||||
|
{ forceSave }?: any
|
||||||
|
): Promise<any>;
|
||||||
|
export function removeMessage(id: string, { Message }?: any): Promise<any>;
|
||||||
|
export function getUnreadByConversation(
|
||||||
|
conversationId: string,
|
||||||
|
{ MessageCollection }?: any
|
||||||
|
): Promise<any>;
|
||||||
|
export function removeAllMessagesInConversation(
|
||||||
|
conversationId: string,
|
||||||
|
{ MessageCollection }?: any
|
||||||
|
): Promise<void>;
|
||||||
|
|
||||||
|
export function getMessageBySender(
|
||||||
|
{
|
||||||
|
source,
|
||||||
|
sourceDevice,
|
||||||
|
sent_at,
|
||||||
|
}: { source: any; sourceDevice: any; sent_at: any },
|
||||||
|
{ Message }: { Message: any }
|
||||||
|
): Promise<any>;
|
||||||
|
export function getMessageIdsFromServerIds(
|
||||||
|
serverIds: any,
|
||||||
|
conversationId: any
|
||||||
|
): Promise<any>;
|
||||||
|
export function getMessageById(
|
||||||
|
id: string,
|
||||||
|
{ Message }: { Message: any }
|
||||||
|
): Promise<any>;
|
||||||
|
export function getAllMessages({
|
||||||
|
MessageCollection,
|
||||||
|
}: {
|
||||||
|
MessageCollection: any;
|
||||||
|
}): Promise<any>;
|
||||||
|
export function getAllUnsentMessages({
|
||||||
|
MessageCollection,
|
||||||
|
}: {
|
||||||
|
MessageCollection: any;
|
||||||
|
}): Promise<any>;
|
||||||
|
export function getAllMessageIds(): Promise<any>;
|
||||||
|
export function getMessagesBySentAt(
|
||||||
|
sentAt: any,
|
||||||
|
{ MessageCollection }: { MessageCollection: any }
|
||||||
|
): Promise<any>;
|
||||||
|
export function getExpiredMessages({
|
||||||
|
MessageCollection,
|
||||||
|
}: {
|
||||||
|
MessageCollection: any;
|
||||||
|
}): Promise<any>;
|
||||||
|
export function getOutgoingWithoutExpiresAt({
|
||||||
|
MessageCollection,
|
||||||
|
}: any): Promise<any>;
|
||||||
|
export function getNextExpiringMessage({
|
||||||
|
MessageCollection,
|
||||||
|
}: {
|
||||||
|
MessageCollection: any;
|
||||||
|
}): Promise<any>;
|
||||||
|
export function getNextExpiringMessage({
|
||||||
|
MessageCollection,
|
||||||
|
}: {
|
||||||
|
MessageCollection: any;
|
||||||
|
}): Promise<any>;
|
||||||
|
export function getMessagesByConversation(
|
||||||
|
conversationId: any,
|
||||||
|
{
|
||||||
|
limit,
|
||||||
|
receivedAt,
|
||||||
|
MessageCollection,
|
||||||
|
type,
|
||||||
|
}: {
|
||||||
|
limit?: number;
|
||||||
|
receivedAt?: number;
|
||||||
|
MessageCollection: any;
|
||||||
|
type?: string;
|
||||||
|
}
|
||||||
|
): Promise<any>;
|
||||||
|
export function getSeenMessagesByHashList(hashes: any): Promise<any>;
|
||||||
|
export function getLastHashBySnode(convoId: any, snode: any): Promise<any>;
|
||||||
|
|
||||||
|
// Unprocessed
|
||||||
|
export function getUnprocessedCount(): Promise<any>;
|
||||||
|
export function getAllUnprocessed(): Promise<any>;
|
||||||
|
export function getUnprocessedById(id: any): Promise<any>;
|
||||||
|
export function saveUnprocessed(
|
||||||
|
data: any,
|
||||||
|
{
|
||||||
|
forceSave,
|
||||||
|
}?: {
|
||||||
|
forceSave: any;
|
||||||
|
}
|
||||||
|
): Promise<any>;
|
||||||
|
export function saveUnprocesseds(
|
||||||
|
arrayOfUnprocessed: any,
|
||||||
|
{
|
||||||
|
forceSave,
|
||||||
|
}?: {
|
||||||
|
forceSave: any;
|
||||||
|
}
|
||||||
|
): Promise<void>;
|
||||||
|
export function updateUnprocessedAttempts(
|
||||||
|
id: any,
|
||||||
|
attempts: any
|
||||||
|
): Promise<void>;
|
||||||
|
export function updateUnprocessedWithData(id: any, data: any): Promise<void>;
|
||||||
|
export function removeUnprocessed(id: any): Promise<void>;
|
||||||
|
export function removeAllUnprocessed(): Promise<void>;
|
||||||
|
|
||||||
|
// Attachment Downloads
|
||||||
|
export function getNextAttachmentDownloadJobs(limit: any): Promise<any>;
|
||||||
|
export function saveAttachmentDownloadJob(job: any): Promise<void>;
|
||||||
|
export function setAttachmentDownloadJobPending(
|
||||||
|
id: any,
|
||||||
|
pending: any
|
||||||
|
): Promise<void>;
|
||||||
|
export function resetAttachmentDownloadPending(): Promise<void>;
|
||||||
|
export function removeAttachmentDownloadJob(id: any): Promise<void>;
|
||||||
|
export function removeAllAttachmentDownloadJobs(): Promise<void>;
|
||||||
|
|
||||||
|
// Other
|
||||||
|
export function removeAll(): Promise<void>;
|
||||||
|
export function removeAllConfiguration(): Promise<void>;
|
||||||
|
export function removeAllConversations(): Promise<void>;
|
||||||
|
export function removeAllPrivateConversations(): Promise<void>;
|
||||||
|
export function removeOtherData(): Promise<void>;
|
||||||
|
export function cleanupOrphanedAttachments(): Promise<void>;
|
||||||
|
|
||||||
|
// Getters
|
||||||
|
export function getMessagesNeedingUpgrade(
|
||||||
|
limit: any,
|
||||||
|
{
|
||||||
|
maxVersion,
|
||||||
|
}: {
|
||||||
|
maxVersion?: number;
|
||||||
|
}
|
||||||
|
): Promise<any>;
|
||||||
|
export function getLegacyMessagesNeedingUpgrade(
|
||||||
|
limit: any,
|
||||||
|
{
|
||||||
|
maxVersion,
|
||||||
|
}: {
|
||||||
|
maxVersion?: number;
|
||||||
|
}
|
||||||
|
): Promise<any>;
|
||||||
|
export function getMessagesWithVisualMediaAttachments(
|
||||||
|
conversationId: any,
|
||||||
|
{
|
||||||
|
limit,
|
||||||
|
}: {
|
||||||
|
limit: any;
|
||||||
|
}
|
||||||
|
): Promise<any>;
|
||||||
|
export function getMessagesWithFileAttachments(
|
||||||
|
conversationId: any,
|
||||||
|
{
|
||||||
|
limit,
|
||||||
|
}: {
|
||||||
|
limit: any;
|
||||||
|
}
|
||||||
|
): Promise<any>;
|
||||||
|
|
||||||
|
// Sender Keys
|
||||||
|
export function getSenderKeys(groupId: any, senderIdentity: any): Promise<any>;
|
||||||
|
export function createOrUpdateSenderKeys(data: any): Promise<void>;
|
||||||
|
@ -0,0 +1,40 @@
|
|||||||
|
import { EncryptionType } from '../types/EncryptionType';
|
||||||
|
import { SignalService } from '../../protobuf';
|
||||||
|
|
||||||
|
function padPlainTextBuffer(messageBuffer: Uint8Array): Uint8Array {
|
||||||
|
const plaintext = new Uint8Array(
|
||||||
|
getPaddedMessageLength(messageBuffer.byteLength + 1) - 1
|
||||||
|
);
|
||||||
|
plaintext.set(new Uint8Array(messageBuffer));
|
||||||
|
plaintext[messageBuffer.byteLength] = 0x80;
|
||||||
|
|
||||||
|
return plaintext;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPaddedMessageLength(originalLength: number): number {
|
||||||
|
const messageLengthWithTerminator = originalLength + 1;
|
||||||
|
let messagePartCount = Math.floor(messageLengthWithTerminator / 160);
|
||||||
|
|
||||||
|
if (messageLengthWithTerminator % 160 !== 0) {
|
||||||
|
messagePartCount += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return messagePartCount * 160;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function encrypt(
|
||||||
|
device: string,
|
||||||
|
plainTextBuffer: Uint8Array,
|
||||||
|
encryptionType: EncryptionType
|
||||||
|
): {
|
||||||
|
envelopeType: SignalService.Envelope.Type;
|
||||||
|
cipherText: Uint8Array;
|
||||||
|
} {
|
||||||
|
const plainText = padPlainTextBuffer(plainTextBuffer);
|
||||||
|
// TODO: Do encryption here?
|
||||||
|
|
||||||
|
return {
|
||||||
|
envelopeType: SignalService.Envelope.Type.CIPHERTEXT,
|
||||||
|
cipherText: new Uint8Array(),
|
||||||
|
};
|
||||||
|
}
|
@ -0,0 +1,3 @@
|
|||||||
|
import * as MessageEncrypter from './MessageEncrypter';
|
||||||
|
|
||||||
|
export { MessageEncrypter };
|
@ -0,0 +1,8 @@
|
|||||||
|
import * as Messages from './messages';
|
||||||
|
import * as Protocols from './protocols';
|
||||||
|
|
||||||
|
// TODO: Do we export class instances here?
|
||||||
|
// E.g
|
||||||
|
// export const messageQueue = new MessageQueue()
|
||||||
|
|
||||||
|
export { Messages, Protocols };
|
@ -0,0 +1,6 @@
|
|||||||
|
// TODO: Populate this with multi device specific code, e.g getting linked devices for a user etc...
|
||||||
|
// We need to deprecate the multi device code we have in js and slowly transition to this file
|
||||||
|
|
||||||
|
export function implementStuffHere() {
|
||||||
|
throw new Error("Don't call me :(");
|
||||||
|
}
|
@ -0,0 +1,57 @@
|
|||||||
|
// TODO: Need to flesh out these functions
|
||||||
|
// Structure of this can be changed for example sticking this all in a class
|
||||||
|
// The reason i haven't done it is to avoid having instances of the protocol, rather you should be able to call the functions directly
|
||||||
|
|
||||||
|
import { OutgoingContentMessage } from '../messages/outgoing';
|
||||||
|
|
||||||
|
export function hasSession(device: string): boolean {
|
||||||
|
return false; // TODO: Implement
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hasSentSessionRequest(device: string): boolean {
|
||||||
|
// TODO: need a way to keep track of if we've sent a session request
|
||||||
|
// My idea was to use the timestamp of when it was sent but there might be another better approach
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function sendSessionRequestIfNeeded(
|
||||||
|
device: string
|
||||||
|
): Promise<void> {
|
||||||
|
if (hasSession(device) || hasSentSessionRequest(device)) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Call sendSessionRequest with SessionReset
|
||||||
|
return Promise.reject(new Error('Need to implement this function'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Replace OutgoingContentMessage with SessionReset
|
||||||
|
export async function sendSessionRequest(
|
||||||
|
message: OutgoingContentMessage
|
||||||
|
): Promise<void> {
|
||||||
|
// TODO: Optimistically store timestamp of when session request was sent
|
||||||
|
// TODO: Send out the request via MessageSender
|
||||||
|
// TODO: On failure, unset the timestamp
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sessionEstablished(device: string) {
|
||||||
|
// TODO: this is called when we receive an encrypted message from the other user
|
||||||
|
// Maybe it should be renamed to something else
|
||||||
|
// TODO: This should make `hasSentSessionRequest` return `false`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function shouldProcessSessionRequest(
|
||||||
|
device: string,
|
||||||
|
messageTimestamp: number
|
||||||
|
): boolean {
|
||||||
|
// TODO: Need to do the following here
|
||||||
|
// messageTimestamp > session request sent timestamp && messageTimestamp > session request processed timestamp
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sessionRequestProcessed(device: string) {
|
||||||
|
// TODO: this is called when we process the session request
|
||||||
|
// This should store the processed timestamp
|
||||||
|
// Again naming is crap so maybe some other name is better
|
||||||
|
}
|
@ -0,0 +1,4 @@
|
|||||||
|
import * as SessionProtocol from './SessionProtocol';
|
||||||
|
import * as MultiDeviceProtocol from './MultiDeviceProtocol';
|
||||||
|
|
||||||
|
export { SessionProtocol, MultiDeviceProtocol };
|
@ -0,0 +1,60 @@
|
|||||||
|
import { EventEmitter } from 'events';
|
||||||
|
import {
|
||||||
|
MessageQueueInterface,
|
||||||
|
MessageQueueInterfaceEvents,
|
||||||
|
} from './MessageQueueInterface';
|
||||||
|
import { OpenGroupMessage, OutgoingContentMessage } from '../messages/outgoing';
|
||||||
|
import { PendingMessageCache } from './PendingMessageCache';
|
||||||
|
import { JobQueue, TypedEventEmitter } from '../utils';
|
||||||
|
|
||||||
|
export class MessageQueue implements MessageQueueInterface {
|
||||||
|
public readonly events: TypedEventEmitter<MessageQueueInterfaceEvents>;
|
||||||
|
private readonly jobQueues: Map<string, JobQueue> = new Map();
|
||||||
|
private readonly cache: PendingMessageCache;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.events = new EventEmitter();
|
||||||
|
this.cache = new PendingMessageCache();
|
||||||
|
this.processAllPending();
|
||||||
|
}
|
||||||
|
|
||||||
|
public sendUsingMultiDevice(user: string, message: OutgoingContentMessage) {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
public send(device: string, message: OutgoingContentMessage) {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
public sendToGroup(message: OutgoingContentMessage | OpenGroupMessage) {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
public sendSyncMessage(message: OutgoingContentMessage) {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public processPending(device: string) {
|
||||||
|
// TODO: implement
|
||||||
|
}
|
||||||
|
|
||||||
|
private processAllPending() {
|
||||||
|
// TODO: Get all devices which are pending here
|
||||||
|
}
|
||||||
|
|
||||||
|
private queue(device: string, message: OutgoingContentMessage) {
|
||||||
|
// TODO: implement
|
||||||
|
}
|
||||||
|
|
||||||
|
private queueOpenGroupMessage(message: OpenGroupMessage) {
|
||||||
|
// TODO: Do we need to queue open group messages?
|
||||||
|
// If so we can get open group job queue and add the send job here
|
||||||
|
}
|
||||||
|
|
||||||
|
private getJobQueue(device: string): JobQueue {
|
||||||
|
let queue = this.jobQueues.get(device);
|
||||||
|
if (!queue) {
|
||||||
|
queue = new JobQueue();
|
||||||
|
this.jobQueues.set(device, queue);
|
||||||
|
}
|
||||||
|
|
||||||
|
return queue;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,19 @@
|
|||||||
|
import { OpenGroupMessage, OutgoingContentMessage } from '../messages/outgoing';
|
||||||
|
import { RawMessage } from '../types/RawMessage';
|
||||||
|
import { TypedEventEmitter } from '../utils';
|
||||||
|
|
||||||
|
// TODO: add all group messages here, replace OutgoingContentMessage with them
|
||||||
|
type GroupMessageType = OpenGroupMessage | OutgoingContentMessage;
|
||||||
|
|
||||||
|
export interface MessageQueueInterfaceEvents {
|
||||||
|
success: (message: RawMessage) => void;
|
||||||
|
fail: (message: RawMessage, error: Error) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MessageQueueInterface {
|
||||||
|
events: TypedEventEmitter<MessageQueueInterfaceEvents>;
|
||||||
|
sendUsingMultiDevice(user: string, message: OutgoingContentMessage): void;
|
||||||
|
send(device: string, message: OutgoingContentMessage): void;
|
||||||
|
sendToGroup(message: GroupMessageType): void;
|
||||||
|
sendSyncMessage(message: OutgoingContentMessage): void;
|
||||||
|
}
|
@ -0,0 +1,14 @@
|
|||||||
|
// REMOVE COMMENT AFTER: This can just export pure functions as it doesn't need state
|
||||||
|
|
||||||
|
import { RawMessage } from '../types/RawMessage';
|
||||||
|
import { OpenGroupMessage } from '../messages/outgoing';
|
||||||
|
|
||||||
|
export async function send(message: RawMessage): Promise<void> {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function sendToOpenGroup(
|
||||||
|
message: OpenGroupMessage
|
||||||
|
): Promise<void> {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
@ -0,0 +1,36 @@
|
|||||||
|
import { RawMessage } from '../types/RawMessage';
|
||||||
|
import { OutgoingContentMessage } from '../messages/outgoing';
|
||||||
|
|
||||||
|
// TODO: We should be able to import functions straight from the db here without going through the window object
|
||||||
|
|
||||||
|
export class PendingMessageCache {
|
||||||
|
private readonly cachedMessages: Array<RawMessage> = [];
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
// TODO: We should load pending messages from db here
|
||||||
|
}
|
||||||
|
|
||||||
|
public addPendingMessage(
|
||||||
|
device: string,
|
||||||
|
message: OutgoingContentMessage
|
||||||
|
): RawMessage {
|
||||||
|
// TODO: Maybe have a util for converting OutgoingContentMessage to RawMessage?
|
||||||
|
// TODO: Raw message has uuid, how are we going to set that? maybe use a different identifier?
|
||||||
|
// One could be device + timestamp would make a unique identifier
|
||||||
|
// TODO: Return previous pending message if it exists
|
||||||
|
return {} as RawMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
public removePendingMessage(message: RawMessage) {
|
||||||
|
// TODO: implement
|
||||||
|
}
|
||||||
|
|
||||||
|
public getPendingDevices(): Array<String> {
|
||||||
|
// TODO: this should return all devices which have pending messages
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public getPendingMessages(device: string): Array<RawMessage> {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,6 @@
|
|||||||
|
// TS 3.8 supports export * as X from 'Y'
|
||||||
|
import * as MessageSender from './MessageSender';
|
||||||
|
export { MessageSender };
|
||||||
|
|
||||||
|
export * from './MessageQueue';
|
||||||
|
export * from './MessageQueueInterface';
|
@ -0,0 +1,5 @@
|
|||||||
|
export enum EncryptionType {
|
||||||
|
Signal,
|
||||||
|
SessionReset,
|
||||||
|
MediumGroup,
|
||||||
|
}
|
@ -0,0 +1,12 @@
|
|||||||
|
import { EncryptionType } from './EncryptionType';
|
||||||
|
|
||||||
|
// TODO: Should we store failure count on raw messages??
|
||||||
|
// Might be better to have a seperate interface which takes in a raw message aswell as a failure count
|
||||||
|
export interface RawMessage {
|
||||||
|
identifier: string;
|
||||||
|
plainTextBuffer: Uint8Array;
|
||||||
|
timestamp: number;
|
||||||
|
device: string;
|
||||||
|
ttl: number;
|
||||||
|
encryption: EncryptionType;
|
||||||
|
}
|
@ -0,0 +1,47 @@
|
|||||||
|
import { v4 as uuid } from 'uuid';
|
||||||
|
|
||||||
|
type Job<ResultType> = (() => PromiseLike<ResultType>) | (() => ResultType);
|
||||||
|
|
||||||
|
// TODO: This needs to replace js/modules/job_queue.js
|
||||||
|
export class JobQueue {
|
||||||
|
private pending: Promise<any> = Promise.resolve();
|
||||||
|
private readonly jobs: Map<string, Promise<unknown>> = new Map();
|
||||||
|
|
||||||
|
public has(id: string): boolean {
|
||||||
|
return this.jobs.has(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async add<Result>(job: Job<Result>): Promise<Result> {
|
||||||
|
const id = uuid();
|
||||||
|
|
||||||
|
return this.addWithId(id, job);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async addWithId<Result>(
|
||||||
|
id: string,
|
||||||
|
job: Job<Result>
|
||||||
|
): Promise<Result> {
|
||||||
|
if (this.jobs.has(id)) {
|
||||||
|
return this.jobs.get(id) as Promise<Result>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const previous = this.pending || Promise.resolve();
|
||||||
|
this.pending = previous.then(job, job);
|
||||||
|
|
||||||
|
const current = this.pending;
|
||||||
|
void current
|
||||||
|
.catch(() => {
|
||||||
|
// This is done to avoid UnhandledPromiseError
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
if (this.pending === current) {
|
||||||
|
delete this.pending;
|
||||||
|
}
|
||||||
|
this.jobs.delete(id);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.jobs.set(id, current);
|
||||||
|
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,53 @@
|
|||||||
|
// Code from https://github.com/andywer/typed-emitter
|
||||||
|
|
||||||
|
type Arguments<T> = [T] extends [(...args: infer U) => any]
|
||||||
|
? U
|
||||||
|
: [T] extends [void] ? [] : [T];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type-safe event emitter.
|
||||||
|
*
|
||||||
|
* Use it like this:
|
||||||
|
*
|
||||||
|
* interface MyEvents {
|
||||||
|
* error: (error: Error) => void
|
||||||
|
* message: (from: string, content: string) => void
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* const myEmitter = new EventEmitter() as TypedEmitter<MyEvents>
|
||||||
|
*
|
||||||
|
* myEmitter.on("message", (from, content) => {
|
||||||
|
* // ...
|
||||||
|
* })
|
||||||
|
*
|
||||||
|
* myEmitter.emit("error", "x") // <- Will catch this type error
|
||||||
|
*
|
||||||
|
* or
|
||||||
|
*
|
||||||
|
* class MyEmitter extends EventEmitter implements TypedEventEmitter<MyEvents>
|
||||||
|
*/
|
||||||
|
export interface TypedEventEmitter<Events> {
|
||||||
|
addListener<E extends keyof Events>(event: E, listener: Events[E]): this;
|
||||||
|
on<E extends keyof Events>(event: E, listener: Events[E]): this;
|
||||||
|
once<E extends keyof Events>(event: E, listener: Events[E]): this;
|
||||||
|
prependListener<E extends keyof Events>(event: E, listener: Events[E]): this;
|
||||||
|
prependOnceListener<E extends keyof Events>(
|
||||||
|
event: E,
|
||||||
|
listener: Events[E]
|
||||||
|
): this;
|
||||||
|
|
||||||
|
off<E extends keyof Events>(event: E, listener: Events[E]): this;
|
||||||
|
removeAllListeners<E extends keyof Events>(event?: E): this;
|
||||||
|
removeListener<E extends keyof Events>(event: E, listener: Events[E]): this;
|
||||||
|
|
||||||
|
emit<E extends keyof Events>(
|
||||||
|
event: E,
|
||||||
|
...args: Arguments<Events[E]>
|
||||||
|
): boolean;
|
||||||
|
eventNames(): Array<keyof Events | string | symbol>;
|
||||||
|
listeners<E extends keyof Events>(event: E): Array<Function>;
|
||||||
|
listenerCount<E extends keyof Events>(event: E): number;
|
||||||
|
|
||||||
|
getMaxListeners(): number;
|
||||||
|
setMaxListeners(maxListeners: number): this;
|
||||||
|
}
|
@ -0,0 +1,2 @@
|
|||||||
|
export * from './TypedEmitter';
|
||||||
|
export * from './JobQueue';
|
@ -0,0 +1,113 @@
|
|||||||
|
import chai from 'chai';
|
||||||
|
import { v4 as uuid } from 'uuid';
|
||||||
|
import { JobQueue } from '../../../session/utils/JobQueue';
|
||||||
|
import { timeout } from '../../utils/timeout';
|
||||||
|
|
||||||
|
// tslint:disable-next-line: no-require-imports no-var-requires
|
||||||
|
const chaiAsPromised = require('chai-as-promised');
|
||||||
|
chai.use(chaiAsPromised);
|
||||||
|
|
||||||
|
const { assert } = chai;
|
||||||
|
|
||||||
|
describe('JobQueue', () => {
|
||||||
|
describe('has', () => {
|
||||||
|
it('should return the correct value', async () => {
|
||||||
|
const queue = new JobQueue();
|
||||||
|
const id = 'jobId';
|
||||||
|
|
||||||
|
assert.isFalse(queue.has(id));
|
||||||
|
const promise = queue.addWithId(id, async () => timeout(100));
|
||||||
|
assert.isTrue(queue.has(id));
|
||||||
|
await promise;
|
||||||
|
assert.isFalse(queue.has(id));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('addWithId', () => {
|
||||||
|
it('should run the jobs concurrently', async () => {
|
||||||
|
const input = [[10, 300], [20, 200], [30, 100]];
|
||||||
|
const queue = new JobQueue();
|
||||||
|
const mapper = async ([value, ms]: Array<number>): Promise<number> =>
|
||||||
|
queue.addWithId(uuid(), async () => {
|
||||||
|
await timeout(ms);
|
||||||
|
|
||||||
|
return value;
|
||||||
|
});
|
||||||
|
|
||||||
|
const start = Date.now();
|
||||||
|
await assert.eventually.deepEqual(Promise.all(input.map(mapper)), [
|
||||||
|
10,
|
||||||
|
20,
|
||||||
|
30,
|
||||||
|
]);
|
||||||
|
const timeTaken = Date.now() - start;
|
||||||
|
assert.closeTo(timeTaken, 600, 50, 'Queue was delayed');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the result of the job', async () => {
|
||||||
|
const queue = new JobQueue();
|
||||||
|
const success = queue.addWithId(uuid(), async () => {
|
||||||
|
await timeout(100);
|
||||||
|
|
||||||
|
return 'success';
|
||||||
|
});
|
||||||
|
const failure = queue.addWithId(uuid(), async () => {
|
||||||
|
await timeout(100);
|
||||||
|
throw new Error('failed');
|
||||||
|
});
|
||||||
|
|
||||||
|
await assert.eventually.equal(success, 'success');
|
||||||
|
await assert.isRejected(failure, /failed/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle sync and async tasks', async () => {
|
||||||
|
const queue = new JobQueue();
|
||||||
|
const first = queue.addWithId(uuid(), () => 'first');
|
||||||
|
const second = queue.addWithId(uuid(), async () => {
|
||||||
|
await timeout(100);
|
||||||
|
|
||||||
|
return 'second';
|
||||||
|
});
|
||||||
|
const third = queue.addWithId(uuid(), () => 'third');
|
||||||
|
|
||||||
|
await assert.eventually.deepEqual(Promise.all([first, second, third]), [
|
||||||
|
'first',
|
||||||
|
'second',
|
||||||
|
'third',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the previous job if same id was passed', async () => {
|
||||||
|
const queue = new JobQueue();
|
||||||
|
const id = uuid();
|
||||||
|
const job = async () => {
|
||||||
|
await timeout(100);
|
||||||
|
|
||||||
|
return 'job1';
|
||||||
|
};
|
||||||
|
|
||||||
|
const promise = queue.addWithId(id, job);
|
||||||
|
const otherPromise = queue.addWithId(id, () => 'job2');
|
||||||
|
await assert.eventually.equal(promise, 'job1');
|
||||||
|
await assert.eventually.equal(otherPromise, 'job1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove completed jobs', async () => {
|
||||||
|
const queue = new JobQueue();
|
||||||
|
const id = uuid();
|
||||||
|
|
||||||
|
const successfullJob = queue.addWithId(id, async () => timeout(100));
|
||||||
|
assert.isTrue(queue.has(id));
|
||||||
|
await successfullJob;
|
||||||
|
assert.isFalse(queue.has(id));
|
||||||
|
|
||||||
|
const failJob = queue.addWithId(id, async () => {
|
||||||
|
await timeout(100);
|
||||||
|
throw new Error('failed');
|
||||||
|
});
|
||||||
|
assert.isTrue(queue.has(id));
|
||||||
|
await assert.isRejected(failJob, /failed/);
|
||||||
|
assert.isFalse(queue.has(id));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,4 @@
|
|||||||
|
export async function timeout(ms: number): Promise<void> {
|
||||||
|
// tslint:disable-next-line no-string-based-set-timeout
|
||||||
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
|
}
|
@ -0,0 +1,120 @@
|
|||||||
|
import { LocalizerType } from './types/Util';
|
||||||
|
|
||||||
|
interface Window {
|
||||||
|
seedNodeList: any;
|
||||||
|
|
||||||
|
WebAPI: any;
|
||||||
|
LokiSnodeAPI: any;
|
||||||
|
SenderKeyAPI: any;
|
||||||
|
LokiMessageAPI: any;
|
||||||
|
StubMessageAPI: any;
|
||||||
|
StubAppDotNetApi: any;
|
||||||
|
LokiPublicChatAPI: any;
|
||||||
|
LokiAppDotNetServerAPI: any;
|
||||||
|
LokiFileServerAPI: any;
|
||||||
|
LokiRssAPI: any;
|
||||||
|
|
||||||
|
CONSTANTS: any;
|
||||||
|
versionInfo: any;
|
||||||
|
|
||||||
|
Events: any;
|
||||||
|
Lodash: any;
|
||||||
|
clearLocalData: any;
|
||||||
|
getAccountManager: any;
|
||||||
|
getConversations: any;
|
||||||
|
getFriendsFromContacts: any;
|
||||||
|
mnemonic: any;
|
||||||
|
clipboard: any;
|
||||||
|
attemptConnection: any;
|
||||||
|
|
||||||
|
passwordUtil: any;
|
||||||
|
userConfig: any;
|
||||||
|
shortenPubkey: any;
|
||||||
|
|
||||||
|
dcodeIO: any;
|
||||||
|
libsignal: any;
|
||||||
|
libloki: any;
|
||||||
|
displayNameRegex: any;
|
||||||
|
|
||||||
|
Signal: any;
|
||||||
|
Whisper: any;
|
||||||
|
ConversationController: any;
|
||||||
|
|
||||||
|
onLogin: any;
|
||||||
|
setPassword: any;
|
||||||
|
textsecure: any;
|
||||||
|
Session: any;
|
||||||
|
log: any;
|
||||||
|
i18n: LocalizerType;
|
||||||
|
friends: any;
|
||||||
|
generateID: any;
|
||||||
|
storage: any;
|
||||||
|
pushToast: any;
|
||||||
|
|
||||||
|
confirmationDialog: any;
|
||||||
|
showQRDialog: any;
|
||||||
|
showSeedDialog: any;
|
||||||
|
showPasswordDialog: any;
|
||||||
|
showEditProfileDialog: any;
|
||||||
|
|
||||||
|
deleteAccount: any;
|
||||||
|
|
||||||
|
toggleTheme: any;
|
||||||
|
toggleMenuBar: any;
|
||||||
|
toggleSpellCheck: any;
|
||||||
|
toggleLinkPreview: any;
|
||||||
|
toggleMediaPermissions: any;
|
||||||
|
|
||||||
|
getSettingValue: any;
|
||||||
|
setSettingValue: any;
|
||||||
|
lokiFeatureFlags: any;
|
||||||
|
|
||||||
|
resetDatabase: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare const window: Window;
|
||||||
|
|
||||||
|
// Utilities
|
||||||
|
export const WebAPI = window.WebAPI;
|
||||||
|
export const Events = window.Events;
|
||||||
|
export const Signal = window.Signal;
|
||||||
|
export const Whisper = window.Whisper;
|
||||||
|
export const ConversationController = window.ConversationController;
|
||||||
|
export const passwordUtil = window.passwordUtil;
|
||||||
|
|
||||||
|
// Values
|
||||||
|
export const CONSTANTS = window.CONSTANTS;
|
||||||
|
export const versionInfo = window.versionInfo;
|
||||||
|
export const mnemonic = window.mnemonic;
|
||||||
|
export const lokiFeatureFlags = window.lokiFeatureFlags;
|
||||||
|
|
||||||
|
// Getters
|
||||||
|
export const getAccountManager = window.getAccountManager;
|
||||||
|
export const getConversations = window.getConversations;
|
||||||
|
export const getFriendsFromContacts = window.getFriendsFromContacts;
|
||||||
|
export const getSettingValue = window.getSettingValue;
|
||||||
|
|
||||||
|
// Setters
|
||||||
|
export const setPassword = window.setPassword;
|
||||||
|
export const setSettingValue = window.setSettingValue;
|
||||||
|
|
||||||
|
// UI Events
|
||||||
|
export const pushToast = window.pushToast;
|
||||||
|
export const confirmationDialog = window.confirmationDialog;
|
||||||
|
|
||||||
|
export const showQRDialog = window.showQRDialog;
|
||||||
|
export const showSeedDialog = window.showSeedDialog;
|
||||||
|
export const showPasswordDialog = window.showPasswordDialog;
|
||||||
|
export const showEditProfileDialog = window.showEditProfileDialog;
|
||||||
|
|
||||||
|
export const toggleTheme = window.toggleTheme;
|
||||||
|
export const toggleMenuBar = window.toggleMenuBar;
|
||||||
|
export const toggleSpellCheck = window.toggleSpellCheck;
|
||||||
|
export const toggleLinkPreview = window.toggleLinkPreview;
|
||||||
|
export const toggleMediaPermissions = window.toggleMediaPermissions;
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
export const clearLocalData = window.clearLocalData;
|
||||||
|
export const deleteAccount = window.deleteAccount;
|
||||||
|
export const resetDatabase = window.resetDatabase;
|
||||||
|
export const attemptConnection = window.attemptConnection;
|
Loading…
Reference in New Issue