├── sound ├── Ping1.mp3 ├── Rotary-Phone6.mp3 ├── incomingMessage.mp3 └── credential ├── example ├── favicon.ico ├── ajax │ └── getturncredentials.json ├── css │ ├── images │ │ ├── ui-icons_444444_256x240.png │ │ ├── ui-icons_555555_256x240.png │ │ ├── ui-icons_777620_256x240.png │ │ ├── ui-icons_777777_256x240.png │ │ ├── ui-icons_cc0000_256x240.png │ │ └── ui-icons_ffffff_256x240.png │ └── example.css ├── fonts │ ├── glyphicons-halflings-regular.eot │ ├── glyphicons-halflings-regular.ttf │ ├── glyphicons-halflings-regular.woff │ └── glyphicons-halflings-regular.woff2 ├── js │ └── example.js └── login.html ├── images ├── loading.gif ├── XMPP_logo.png ├── composing.png ├── filetypes │ ├── audio.png │ ├── file.png │ ├── folder.png │ ├── image.png │ ├── text.png │ ├── video.png │ ├── application.png │ ├── text-code.png │ ├── text-vcard.png │ ├── folder-public.png │ ├── folder-shared.png │ ├── text-calendar.png │ ├── application-pdf.png │ ├── folder-external.png │ ├── folder-starred.png │ ├── folder-drag-accept.png │ ├── package-x-generic.png │ ├── x-office-document.png │ ├── x-office-presentation.png │ ├── x-office-spreadsheet.png │ ├── text-code.svg │ ├── folder-drag-accept.svg │ ├── video.svg │ ├── x-office-presentation.svg │ ├── package-x-generic.svg │ ├── folder.svg │ ├── folder-starred.svg │ ├── file.svg │ ├── text.svg │ ├── image.svg │ ├── x-office-document.svg │ ├── x-office-spreadsheet.svg │ ├── folder-external.svg │ ├── audio.svg │ ├── text-calendar.svg │ ├── folder-shared.svg │ ├── folder-public.svg │ └── application.svg ├── menu_black.svg ├── camera_icon_black.svg ├── menu_white.svg ├── camera_icon_grey.svg ├── bookmark_black.svg ├── bookmark_white.svg ├── camera_icon_white.svg ├── edit_black.svg ├── fullscreen_black.svg ├── edit_white.svg ├── fullscreen_white.svg ├── bookmark_red.svg ├── location_icon.svg ├── speech_balloon_black.svg ├── close_black.svg ├── speech_balloon_white.svg ├── delete_black.svg ├── close_white.svg ├── delete_white.svg ├── resize_gray.svg ├── help_black.svg ├── more_black.svg ├── help_white.svg ├── more_white.svg ├── info_black.svg ├── info_white.svg ├── bell.svg ├── hang_up_black.svg ├── hang_up_red.svg ├── hang_up_white.svg ├── pick_up_black.svg ├── pick_up_green.svg ├── pick_up_white.svg ├── microphone_black.svg ├── attachment.svg ├── microphone_white.svg ├── camera_disabled_icon_black.svg ├── dragover_white.svg ├── drop_white.svg ├── camera_disabled_icon_white.svg ├── padlock_close_green.svg ├── padlock_close_orange.svg ├── padlock_open_black.svg ├── padlock_open_white.svg ├── minimize_black.svg ├── contact_black.svg ├── contact_white.svg ├── download_icon_black.svg ├── download_icon_white.svg ├── megaphone_icon_black.svg ├── smiley.svg ├── microphone_disabled_black.svg ├── microphone_disabled_white.svg ├── pick_up_white_disabled.svg ├── mute_black.svg ├── padlock_open_disabled_black.svg ├── presence │ └── online.svg └── XMPP_logo.svg ├── src ├── LinkHandler.interface.ts ├── plugins │ ├── omemo │ │ ├── model │ │ │ ├── Exportable.ts │ │ │ ├── RegistrationId.ts │ │ │ ├── SignedPreKey.ts │ │ │ ├── EncryptedDeviceMessage.ts │ │ │ ├── PreKey.ts │ │ │ └── IdentityKey.ts │ │ ├── util │ │ │ ├── Formatter.ts │ │ │ ├── Const.ts │ │ │ └── ArrayBuffer.ts │ │ ├── vendor │ │ │ ├── Address.ts │ │ │ ├── SignalStore.interface.ts │ │ │ ├── SessionCipher.ts │ │ │ ├── SessionBuilder.ts │ │ │ ├── Signal.ts │ │ │ └── KeyHelper.ts │ │ └── lib │ │ │ └── IdentityManager.ts │ ├── chatState │ │ ├── State.ts │ │ └── ChatStateConnection.ts │ ├── bookmarks │ │ ├── services │ │ │ ├── AbstractService.ts │ │ │ └── LocalService.ts │ │ ├── RoomBookmark.ts │ │ └── BookmarksPlugin.ts │ └── MeCommandPlugin.ts ├── Identifiable.interface.ts ├── errors │ ├── ParsingError.ts │ ├── ConnectionError.ts │ ├── AuthenticationError.ts │ ├── InvalidParameterError.ts │ └── BaseError.ts ├── index.ts ├── connection │ ├── FormItemField.ts │ ├── FormReportedField.ts │ ├── xmpp │ │ ├── AbstractHandler.ts │ │ ├── JingleHandler.ts │ │ ├── namespace.ts │ │ └── handlers │ │ │ ├── multiUser │ │ │ ├── Presence.ts │ │ │ ├── XMessage.ts │ │ │ └── DirectInvitation.ts │ │ │ ├── headlineMessage.ts │ │ │ └── jingle.ts │ └── services │ │ ├── AbstractService.ts │ │ ├── Disco.ts │ │ └── PEP.ts ├── Avatar.interface.ts ├── util │ ├── Log.interface.ts │ ├── Hash.ts │ ├── Color.ts │ ├── UUID.ts │ ├── PersistentArray.ts │ ├── Random.ts │ ├── HookRepository.ts │ ├── Pipe.ts │ ├── Translation.ts │ └── Utils.ts ├── plugin │ └── EncryptionPlugin.ts ├── ui │ ├── util │ │ ├── ByteBeautifier.ts │ │ ├── DateTime.ts │ │ ├── ElementHandler.ts │ │ └── TableElement.ts │ ├── Overlay.ts │ ├── dialogs │ │ ├── notification.ts │ │ ├── fingerprints.ts │ │ ├── about.ts │ │ ├── multiUserInvite.ts │ │ ├── confirm.ts │ │ ├── unknownSender.ts │ │ ├── selection.ts │ │ └── multiUserInvitation.ts │ ├── web.ts │ ├── DialogList.ts │ ├── DialogNavigation.ts │ ├── DialogSection.ts │ ├── DialogPage.ts │ ├── DialogListItem.ts │ └── ChatWindowFileTransferHandler.ts ├── StorableAbstract.ts ├── JID.interface.ts ├── JingleMediaSession.ts ├── DiscoInfo.interface.ts ├── bootstrap │ ├── webpackPublicPath.ts │ └── plugins.ts ├── api │ ├── v1 │ │ ├── disconnect.ts │ │ └── debug.ts │ └── index.ts ├── Client.interface.ts ├── ContactProvider.ts ├── vendor │ └── Strophe.ts ├── DiscoInfoRepository.interface.ts ├── PageVisibility.ts ├── Storage.interface.ts ├── LinkHandlerGeo.ts ├── CONST.ts ├── StateMachine.ts ├── Migration.ts ├── FallbackContactProvider.ts ├── DiscoInfoChangeable.ts ├── Avatar.ts └── JingleStreamSession.ts ├── scss ├── modules │ ├── _all.scss │ ├── _muc.scss │ ├── _webrtc.scss │ ├── _presence.scss │ ├── _scrollbar.scss │ └── _animation.scss ├── main.scss └── partials │ ├── _emoticons.scss │ ├── _avatar.scss │ └── _button.scss ├── template ├── notification.hbs ├── chatWindowList.hbs ├── vcard-body.hbs ├── confirm.hbs ├── helpers │ ├── breaklines.js │ ├── t.js │ └── tr.js ├── partials │ └── menu.hbs ├── dialog.hbs ├── chat-window-message.hbs ├── dialogOmemoDeviceList.hbs ├── debugLog.hbs ├── vcard.hbs ├── roster-form.hbs ├── selection.hbs ├── fingerprints.hbs ├── dialogOmemoDeviceItem.hbs ├── roster-item.hbs ├── videoDialog.hbs ├── about.hbs ├── multiUserInvite.hbs ├── multiUserInvitation.hbs ├── roster.hbs ├── loginBox.hbs ├── contact.hbs └── bookmark.hbs ├── .lgtm.yml ├── .gitignore ├── CONTRIBUTING.md ├── .editorconfig ├── .travis.yml ├── .npmignore ├── test ├── AccountStub.ts ├── connection │ └── xmpp │ │ └── handlers │ │ └── presence.spec.ts └── Client.ts ├── .stylelintrc ├── .commitlintrc.json ├── .vscode └── settings.json ├── .github └── ISSUE_TEMPLATE │ ├── feature-request.md │ └── bug_report.md ├── custom.d.ts ├── tslint.json ├── tsconfig.json ├── LICENSE └── scripts ├── prepare-commit-msg.js └── search-blacklist.js /sound/Ping1.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-it/jsxc/master/sound/Ping1.mp3 -------------------------------------------------------------------------------- /example/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-it/jsxc/master/example/favicon.ico -------------------------------------------------------------------------------- /images/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-it/jsxc/master/images/loading.gif -------------------------------------------------------------------------------- /images/XMPP_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-it/jsxc/master/images/XMPP_logo.png -------------------------------------------------------------------------------- /images/composing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-it/jsxc/master/images/composing.png -------------------------------------------------------------------------------- /sound/Rotary-Phone6.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-it/jsxc/master/sound/Rotary-Phone6.mp3 -------------------------------------------------------------------------------- /images/filetypes/audio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-it/jsxc/master/images/filetypes/audio.png -------------------------------------------------------------------------------- /images/filetypes/file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-it/jsxc/master/images/filetypes/file.png -------------------------------------------------------------------------------- /images/filetypes/folder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-it/jsxc/master/images/filetypes/folder.png -------------------------------------------------------------------------------- /images/filetypes/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-it/jsxc/master/images/filetypes/image.png -------------------------------------------------------------------------------- /images/filetypes/text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-it/jsxc/master/images/filetypes/text.png -------------------------------------------------------------------------------- /images/filetypes/video.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-it/jsxc/master/images/filetypes/video.png -------------------------------------------------------------------------------- /sound/incomingMessage.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-it/jsxc/master/sound/incomingMessage.mp3 -------------------------------------------------------------------------------- /src/LinkHandler.interface.ts: -------------------------------------------------------------------------------- 1 | export interface ILinkHandler { 2 | detect(element: JQuery): void 3 | } 4 | -------------------------------------------------------------------------------- /src/plugins/omemo/model/Exportable.ts: -------------------------------------------------------------------------------- 1 | export default interface IExportable { 2 | export(): any 3 | } 4 | -------------------------------------------------------------------------------- /images/filetypes/application.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-it/jsxc/master/images/filetypes/application.png -------------------------------------------------------------------------------- /images/filetypes/text-code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-it/jsxc/master/images/filetypes/text-code.png -------------------------------------------------------------------------------- /images/filetypes/text-vcard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-it/jsxc/master/images/filetypes/text-vcard.png -------------------------------------------------------------------------------- /images/filetypes/folder-public.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-it/jsxc/master/images/filetypes/folder-public.png -------------------------------------------------------------------------------- /images/filetypes/folder-shared.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-it/jsxc/master/images/filetypes/folder-shared.png -------------------------------------------------------------------------------- /images/filetypes/text-calendar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-it/jsxc/master/images/filetypes/text-calendar.png -------------------------------------------------------------------------------- /images/filetypes/application-pdf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-it/jsxc/master/images/filetypes/application-pdf.png -------------------------------------------------------------------------------- /images/filetypes/folder-external.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-it/jsxc/master/images/filetypes/folder-external.png -------------------------------------------------------------------------------- /images/filetypes/folder-starred.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-it/jsxc/master/images/filetypes/folder-starred.png -------------------------------------------------------------------------------- /images/filetypes/folder-drag-accept.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-it/jsxc/master/images/filetypes/folder-drag-accept.png -------------------------------------------------------------------------------- /images/filetypes/package-x-generic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-it/jsxc/master/images/filetypes/package-x-generic.png -------------------------------------------------------------------------------- /images/filetypes/x-office-document.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-it/jsxc/master/images/filetypes/x-office-document.png -------------------------------------------------------------------------------- /example/ajax/getturncredentials.json: -------------------------------------------------------------------------------- 1 | {"url":"numb.viagenie.ca","username":"webrtc@live.com","credential":"muazkh","ttl":43200} 2 | -------------------------------------------------------------------------------- /images/filetypes/x-office-presentation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-it/jsxc/master/images/filetypes/x-office-presentation.png -------------------------------------------------------------------------------- /images/filetypes/x-office-spreadsheet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-it/jsxc/master/images/filetypes/x-office-spreadsheet.png -------------------------------------------------------------------------------- /src/Identifiable.interface.ts: -------------------------------------------------------------------------------- 1 | 2 | interface IIdentifiable { 3 | getId(): string 4 | } 5 | 6 | export default IIdentifiable; 7 | -------------------------------------------------------------------------------- /example/css/images/ui-icons_444444_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-it/jsxc/master/example/css/images/ui-icons_444444_256x240.png -------------------------------------------------------------------------------- /example/css/images/ui-icons_555555_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-it/jsxc/master/example/css/images/ui-icons_555555_256x240.png -------------------------------------------------------------------------------- /example/css/images/ui-icons_777620_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-it/jsxc/master/example/css/images/ui-icons_777620_256x240.png -------------------------------------------------------------------------------- /example/css/images/ui-icons_777777_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-it/jsxc/master/example/css/images/ui-icons_777777_256x240.png -------------------------------------------------------------------------------- /example/css/images/ui-icons_cc0000_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-it/jsxc/master/example/css/images/ui-icons_cc0000_256x240.png -------------------------------------------------------------------------------- /example/css/images/ui-icons_ffffff_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-it/jsxc/master/example/css/images/ui-icons_ffffff_256x240.png -------------------------------------------------------------------------------- /example/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-it/jsxc/master/example/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /example/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-it/jsxc/master/example/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /example/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-it/jsxc/master/example/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /src/errors/ParsingError.ts: -------------------------------------------------------------------------------- 1 | import BaseError from './BaseError' 2 | 3 | export default class ParsingError extends BaseError { 4 | 5 | } 6 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import './bootstrap/webpackPublicPath' 2 | import './bootstrap/plugins' 3 | import JSXC from './api/' 4 | 5 | export = JSXC 6 | -------------------------------------------------------------------------------- /example/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-it/jsxc/master/example/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /src/connection/FormItemField.ts: -------------------------------------------------------------------------------- 1 | import FormField from './FormField'; 2 | 3 | export default class FormItemField extends FormField { 4 | 5 | } 6 | -------------------------------------------------------------------------------- /src/errors/ConnectionError.ts: -------------------------------------------------------------------------------- 1 | import BaseError from './BaseError' 2 | 3 | export default class ConnectionError extends BaseError { 4 | 5 | } 6 | -------------------------------------------------------------------------------- /src/Avatar.interface.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface IAvatar { 3 | getData(): string 4 | 5 | getType(): string 6 | 7 | getHash(): string 8 | } 9 | -------------------------------------------------------------------------------- /src/connection/FormReportedField.ts: -------------------------------------------------------------------------------- 1 | import FormField from './FormField'; 2 | 3 | export default class FormReportedField extends FormField { 4 | 5 | } 6 | -------------------------------------------------------------------------------- /src/errors/AuthenticationError.ts: -------------------------------------------------------------------------------- 1 | import BaseError from './BaseError' 2 | 3 | export default class AuthenticationError extends BaseError { 4 | 5 | } 6 | -------------------------------------------------------------------------------- /src/errors/InvalidParameterError.ts: -------------------------------------------------------------------------------- 1 | import BaseError from './BaseError' 2 | 3 | export default class InvalidParameterError extends BaseError { 4 | 5 | } 6 | -------------------------------------------------------------------------------- /scss/modules/_all.scss: -------------------------------------------------------------------------------- 1 | @import "colors"; 2 | @import "animation"; 3 | @import "muc"; 4 | @import "webrtc"; 5 | @import "presence"; 6 | @import "scrollbar"; 7 | -------------------------------------------------------------------------------- /scss/modules/_muc.scss: -------------------------------------------------------------------------------- 1 | %muc-avatar-icon { 2 | background-image: url("../images/group_white.svg"); 3 | background-size: 1em; 4 | text-indent: 999px; 5 | } 6 | -------------------------------------------------------------------------------- /images/menu_black.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/camera_icon_black.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/menu_white.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/camera_icon_grey.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /template/notification.hbs: -------------------------------------------------------------------------------- 1 |

{{subject}}

2 | 3 |

{{{breaklines message}}}

4 | 5 | {{#if from}} 6 |

{{t "from"}} {{from}}

7 | {{/if}} 8 | -------------------------------------------------------------------------------- /src/plugins/chatState/State.ts: -------------------------------------------------------------------------------- 1 | export enum STATE { 2 | ACTIVE = 'active', 3 | COMPOSING = 'composing', 4 | PAUSED = 'paused', 5 | INACTIVE = 'inactive', 6 | GONE = 'gone' 7 | }; 8 | -------------------------------------------------------------------------------- /images/bookmark_black.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/bookmark_white.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /scss/modules/_webrtc.scss: -------------------------------------------------------------------------------- 1 | %fullscreen { 2 | background-color: $fullscreen-bg; 3 | height: 100%; 4 | left: 0; 5 | position: absolute; 6 | top: 0; 7 | width: 100%; 8 | z-index: 9000; 9 | } 10 | -------------------------------------------------------------------------------- /.lgtm.yml: -------------------------------------------------------------------------------- 1 | extraction: 2 | javascript: 3 | index: 4 | exclude: 5 | - "build/" 6 | - "lib/" 7 | filters: 8 | - exclude: "src/jsxc.intro.js" 9 | - exclude: "src/jsxc.outro.js" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.zip 3 | *.zip.sig 4 | bower_components 5 | archives/ 6 | /css/ 7 | dev/ 8 | .wti 9 | *~ 10 | /tmp/ 11 | .idea 12 | /bundle.js 13 | /dist/ 14 | yarn-error.log 15 | .stylelintcache 16 | .env 17 | -------------------------------------------------------------------------------- /src/plugins/omemo/model/RegistrationId.ts: -------------------------------------------------------------------------------- 1 | 2 | export default class RegistrationId { 3 | constructor(private id: number) { 4 | 5 | } 6 | 7 | public getId(): number { 8 | return this.id; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /images/camera_icon_white.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/edit_black.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/fullscreen_black.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/plugins/omemo/util/Formatter.ts: -------------------------------------------------------------------------------- 1 | 2 | export default { 3 | toReadableKey: (key: ArrayBuffer) => { 4 | return ( window).dcodeIO.ByteBuffer.wrap(key).toHex(1).toUpperCase().match(/.{1,8}/g).join(' '); 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /images/edit_white.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/fullscreen_white.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /template/chatWindowList.hbs: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |
5 |
<
6 |
>
7 |
8 | -------------------------------------------------------------------------------- /src/util/Log.interface.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface ILog { 3 | info(message: string, ...data): void 4 | 5 | debug(message: string, ...data): void 6 | 7 | warn(message: string, ...data): void 8 | 9 | error(message: string, ...data): void 10 | } 11 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Please note that this project is released with a Contributor Code of Conduct. By participating in this project you agree to abide by its terms. 2 | 3 | You find all information in our [contributor guide](https://github.com/jsxc/jsxc/wiki/Contributor-Guide). 4 | -------------------------------------------------------------------------------- /src/plugin/EncryptionPlugin.ts: -------------------------------------------------------------------------------- 1 | import { AbstractPlugin } from './AbstractPlugin' 2 | import { IContact } from '../Contact.interface' 3 | 4 | export abstract class EncryptionPlugin extends AbstractPlugin { 5 | public abstract toggleTransfer(contact: IContact): Promise; 6 | } 7 | -------------------------------------------------------------------------------- /images/bookmark_red.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/location_icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /template/vcard-body.hbs: -------------------------------------------------------------------------------- 1 | {{#*inline "list"}} 2 | 12 | {{/inline}} 13 | 14 | {{> list}} 15 | -------------------------------------------------------------------------------- /template/confirm.hbs: -------------------------------------------------------------------------------- 1 |

{{t "Are_you_sure"}}

2 | 3 |

{{{question}}}

4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /template/helpers/breaklines.js: -------------------------------------------------------------------------------- 1 | /* jshint node: true */ 2 | var Handlebars = require('handlebars-runtime'); 3 | 4 | module.exports = function(text) { 5 | text = Handlebars.Utils.escapeExpression(text); 6 | text = text.replace(/(\r\n|\n|\r)/gm, '
'); 7 | 8 | return new Handlebars.SafeString(text); 9 | }; 10 | -------------------------------------------------------------------------------- /images/speech_balloon_black.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/errors/BaseError.ts: -------------------------------------------------------------------------------- 1 | export default class BaseError { 2 | constructor(private message: string, private errorCode?: string) { 3 | 4 | } 5 | 6 | public toString(): string { 7 | return this.message; 8 | } 9 | 10 | public getErrorCode(): string { 11 | return this.errorCode; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /images/close_black.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/speech_balloon_white.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /template/helpers/t.js: -------------------------------------------------------------------------------- 1 | /* jshint node: true */ 2 | var Handlebars = require('handlebars-runtime'); 3 | var Translation = require('../../src/util/Translation').default; 4 | 5 | module.exports = function(i18n_key) { 6 | var result = Translation.t(i18n_key); 7 | 8 | return new Handlebars.SafeString(result || i18n_key); 9 | }; 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*] 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [src/**.ts] 12 | charset = utf-8 13 | indent_style = space 14 | indent_size = 3 15 | quote_type = single 16 | -------------------------------------------------------------------------------- /images/delete_black.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: bionic 2 | 3 | language: node_js 4 | 5 | node_js: 6 | - 12 7 | 8 | addons: 9 | chrome: stable 10 | 11 | cache: 12 | yarn: true 13 | directories: 14 | - node_modules 15 | 16 | script: 17 | - yarn checking-style-format 18 | - yarn checking-typescript-format 19 | - commitlint-travis 20 | - yarn test 21 | -------------------------------------------------------------------------------- /src/ui/util/ByteBeautifier.ts: -------------------------------------------------------------------------------- 1 | export default function format(byte: number) { 2 | let s = ['', 'KB', 'MB', 'GB', 'TB']; 3 | let i; 4 | 5 | for (i = 1; i < s.length; i++) { 6 | if (byte < 1024) { 7 | break; 8 | } 9 | byte /= 1024; 10 | } 11 | 12 | return (Math.round(byte * 10) / 10) + s[i - 1]; 13 | } 14 | -------------------------------------------------------------------------------- /images/close_white.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/delete_white.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /template/partials/menu.hbs: -------------------------------------------------------------------------------- 1 |
2 |
{{button-label}}
3 |
4 |
    5 | {{#if @partial-block}} 6 | {{> @partial-block }} 7 | {{/if}} 8 |
9 |
10 |
11 | -------------------------------------------------------------------------------- /template/dialog.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
×
5 | 6 | {{{content}}} 7 | 8 |
9 |
10 |
11 |
12 | -------------------------------------------------------------------------------- /src/connection/xmpp/AbstractHandler.ts: -------------------------------------------------------------------------------- 1 | import Account from '../../Account' 2 | 3 | export default abstract class AbstractHandler { 4 | protected PRESERVE_HANDLER = true; 5 | protected REMOVE_HANDLER = false; 6 | 7 | constructor(protected account: Account) { 8 | 9 | } 10 | 11 | public abstract processStanza(stanza: Element): boolean; 12 | } 13 | -------------------------------------------------------------------------------- /images/resize_gray.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /sound/credential: -------------------------------------------------------------------------------- 1 | Rotary-Phone6.mp3 2 | === 3 | Creator: David English 4 | Source: http://www.beepzoid.com/old-phones/ 5 | License: unknown 6 | 7 | Ping1.mp3 8 | === 9 | Creator: CameronMusic 10 | Source: https://soundcloud.com/freefilmandgamemusic/ping-1?in=freefilmandgamemusic/sets/free-notification-sounds-and 11 | License: cc 3.0, http://creativecommons.org/licenses/by/3.0/ -------------------------------------------------------------------------------- /src/StorableAbstract.ts: -------------------------------------------------------------------------------- 1 | import IIdentifiable from './Identifiable.interface' 2 | 3 | abstract class Storable implements IIdentifiable { 4 | constructor(data: any) { 5 | $.extend(this, data); 6 | } 7 | 8 | public abstract getId(): string; 9 | 10 | public abstract save(); 11 | 12 | public abstract remove(); 13 | } 14 | 15 | export default Storable; 16 | -------------------------------------------------------------------------------- /images/help_black.svg: -------------------------------------------------------------------------------- 1 | ? -------------------------------------------------------------------------------- /images/more_black.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/JID.interface.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface IJID { 3 | readonly full: string; 4 | 5 | readonly bare: string; 6 | 7 | readonly node: string; 8 | 9 | readonly domain: string; 10 | 11 | readonly resource: string; 12 | 13 | toString(): string; 14 | 15 | toEscapedString(): string; 16 | 17 | isBare(): boolean; 18 | 19 | isServer(): boolean; 20 | } 21 | -------------------------------------------------------------------------------- /src/ui/Overlay.ts: -------------------------------------------------------------------------------- 1 | 2 | export default class Overlay { 3 | private element; 4 | 5 | constructor() { 6 | this.element = $('
'); 7 | this.element.addClass('jsxc-overlay jsxc-overlay-black'); 8 | } 9 | 10 | public open() { 11 | this.element.appendTo('body'); 12 | } 13 | 14 | public close() { 15 | this.element.remove(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /images/help_white.svg: -------------------------------------------------------------------------------- 1 | ? -------------------------------------------------------------------------------- /images/more_white.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/ui/dialogs/notification.ts: -------------------------------------------------------------------------------- 1 | import Dialog from '../Dialog' 2 | 3 | let notificationTemplate = require('../../../template/notification.hbs'); 4 | 5 | export default function(subject: string, message: string, from?: string) { 6 | let content = notificationTemplate({ 7 | subject, 8 | message, 9 | from 10 | }); 11 | 12 | let dialog = new Dialog(content); 13 | dialog.open(); 14 | } 15 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | *.zip 2 | *.zip.sig 3 | /archives/ 4 | /.wti 5 | *~ 6 | /tmp/ 7 | .idea 8 | yarn-error.log 9 | /.github/ 10 | /.vscode/ 11 | /example/ 12 | /scripts/ 13 | /test/ 14 | /.commitlintrc.json 15 | /.gitignore 16 | /.lgtm.yml 17 | /.stylelintrc 18 | /.travis.yml 19 | /CODE_OF_CONDUCT.md 20 | /CONTRIBUTING.md 21 | /ISSUE_TEMPLATE.md 22 | /karma.conf.js 23 | /tslint.json 24 | /.editorconfig 25 | /.env 26 | /.stylelintcache 27 | -------------------------------------------------------------------------------- /src/JingleMediaSession.ts: -------------------------------------------------------------------------------- 1 | import JingleAbstractSession from './JingleAbstractSession'; 2 | import { IOTalkJingleMediaSession } from '@vendor/Jingle.interface'; 3 | 4 | export default abstract class JingleMediaSession extends JingleAbstractSession { 5 | protected session: IOTalkJingleMediaSession; 6 | 7 | public getRemoteStreams(): MediaStream[] { 8 | return this.session.pc.getRemoteStreams(); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/plugins/bookmarks/services/AbstractService.ts: -------------------------------------------------------------------------------- 1 | import { IJID } from '@src/JID.interface'; 2 | import RoomBookmark from '../RoomBookmark'; 3 | 4 | export default abstract class AbstractService { 5 | public abstract getName(): string 6 | 7 | public abstract async getRooms(): Promise 8 | 9 | public abstract async addRoom(room: RoomBookmark) 10 | 11 | public abstract async removeRoom(id: IJID) 12 | } 13 | -------------------------------------------------------------------------------- /src/util/Hash.ts: -------------------------------------------------------------------------------- 1 | 2 | export default class Hash { 3 | public static String(value: string) { 4 | let hash = 0; 5 | 6 | if (value.length === 0) { 7 | return hash; 8 | } 9 | 10 | for (let i = 0; i < value.length; i++) { 11 | hash = ((hash << 5) - hash) + value.charCodeAt(i); 12 | hash |= 0; // Convert to 32bit integer 13 | } 14 | 15 | return hash; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /template/chat-window-message.hbs: -------------------------------------------------------------------------------- 1 |
2 |
{{content}}
3 |
4 | 10 |
11 | -------------------------------------------------------------------------------- /src/connection/xmpp/JingleHandler.ts: -------------------------------------------------------------------------------- 1 | import JingleHandler from '../JingleHandler' 2 | import JingleAbstractSession from '../../JingleAbstractSession' 3 | 4 | export default class XMPPJingleHandler extends JingleHandler { 5 | protected onIncoming(session) { 6 | let jingleSession: JingleAbstractSession = super.onIncoming(session); 7 | 8 | jingleSession.onOnceIncoming(); 9 | 10 | return jingleSession 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /template/dialogOmemoDeviceList.hbs: -------------------------------------------------------------------------------- 1 |
2 |

{{t "OMEMO_devices"}}

3 |

{{t "omemo-device-list-explanation"}}

4 | 5 |

{{t "OMEMO_peer_devices"}}

6 |
{{t "Please_wait"}}
7 | 8 |

{{t "OMEMO_own_devices"}}

9 |
{{t "Please_wait"}}
10 |
11 | -------------------------------------------------------------------------------- /images/info_black.svg: -------------------------------------------------------------------------------- 1 | i -------------------------------------------------------------------------------- /src/plugins/omemo/model/SignedPreKey.ts: -------------------------------------------------------------------------------- 1 | import PreKey, { IPreKeyObject } from './PreKey'; 2 | 3 | interface ISignedPreKeyObject extends IPreKeyObject { 4 | signature: ArrayBuffer 5 | } 6 | 7 | export default class SignedPreKey extends PreKey { 8 | constructor(data: ISignedPreKeyObject) { 9 | super(data); 10 | } 11 | 12 | public getSignature(): ArrayBuffer { 13 | return ( this.data).signature; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/ui/web.ts: -------------------------------------------------------------------------------- 1 | import SM from '../StateMachine' 2 | import ChatWindowList from './ChatWindowList' 3 | import Roster from './Roster' 4 | 5 | export function init() { 6 | if (SM.getUIState() === SM.UISTATE.STANDBY) { 7 | SM.changeUIState(SM.UISTATE.INITIATING); 8 | 9 | ChatWindowList.init(); 10 | 11 | Roster.init(); 12 | } 13 | 14 | // if (Options.get('muteNotification')) { 15 | // Notification.muteSound(); 16 | // } 17 | } 18 | -------------------------------------------------------------------------------- /template/debugLog.hbs: -------------------------------------------------------------------------------- 1 |

{{t "User_information"}}

2 |
3 |
4 | {{#each userInfo}} 5 |
{{key}}
6 |
{{value}}
7 | {{/each}} 8 |
9 |
10 | 11 |

{{t "Webcam"}}

12 |
13 | 14 |
15 | 16 |

{{t "Log"}}

17 |
18 | -------------------------------------------------------------------------------- /template/helpers/tr.js: -------------------------------------------------------------------------------- 1 | var Handlebars = require('handlebars-runtime'); 2 | var Translation = require('../../src/util/Translation').default; 3 | 4 | module.exports = function(context, options) { 5 | var opts = i18next.functions.extend(options.hash, context); 6 | 7 | if (options.fn) 8 | opts.defaultValue = options.fn(context); 9 | 10 | var result = Translation.t(opts.key, opts); 11 | 12 | return new Handlebars.SafeString(result); 13 | }; 14 | -------------------------------------------------------------------------------- /images/info_white.svg: -------------------------------------------------------------------------------- 1 | i -------------------------------------------------------------------------------- /src/DiscoInfo.interface.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface IIdentity { 3 | category: string 4 | type: string, 5 | name?: string, 6 | lang?: string 7 | } 8 | 9 | export interface IDiscoInfo { 10 | getIdentities(): IIdentity[] 11 | 12 | getFeatures(): string[] 13 | 14 | getForms() 15 | 16 | getFormByType(type: string) 17 | 18 | getCapsVersion(): String 19 | 20 | hasFeature(features: string[]): boolean 21 | hasFeature(feature: string): boolean 22 | } 23 | -------------------------------------------------------------------------------- /src/plugins/omemo/util/Const.ts: -------------------------------------------------------------------------------- 1 | export const NUM_PRE_KEYS = 50; 2 | export const MAX_PRE_KEY_ID = Math.pow(2, 31) - 1; 3 | 4 | export const MAX_REGISTRATION_ID = Math.pow(2, 31) - 1; 5 | 6 | export const NS_BASE = 'eu.siacs.conversations.axolotl'; 7 | export const NS_DEVICELIST = NS_BASE + '.devicelist'; 8 | export const NS_BUNDLES = NS_BASE + '.bundles:'; 9 | 10 | export const AES_KEY_LENGTH = 128; 11 | export const AES_TAG_LENGTH = 128; 12 | export const AES_EXTRACTABLE = true; 13 | -------------------------------------------------------------------------------- /src/ui/dialogs/fingerprints.ts: -------------------------------------------------------------------------------- 1 | import Dialog from '../Dialog' 2 | 3 | let fingerprintsTemplate = require('../../../template/fingerprints.hbs'); 4 | 5 | export default function(ownFingerprint: string, theirFingerprint: string) { 6 | let content = fingerprintsTemplate({ 7 | ownFingerprint: ownFingerprint.replace(/(.{8})/g, '$1 '), 8 | theirFingerprint: theirFingerprint.replace(/(.{8})/g, '$1 ') 9 | }); 10 | 11 | let dialog = new Dialog(content); 12 | dialog.open(); 13 | } 14 | -------------------------------------------------------------------------------- /src/util/Color.ts: -------------------------------------------------------------------------------- 1 | import * as getRGB from 'consistent-color-generation' 2 | 3 | export default class Color { 4 | public static generate(text: string, correction?: 'redgreen' | 'blue', saturation?: number, lightness?: number) { 5 | let color = getRGB(text, correction, saturation, lightness); 6 | let r = Math.round(color.r * 255); 7 | let g = Math.round(color.g * 255); 8 | let b = Math.round(color.b * 255); 9 | 10 | return `rgb(${r}, ${g}, ${b})`; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /images/bell.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/ui/DialogList.ts: -------------------------------------------------------------------------------- 1 | import ListItem from './DialogListItem' 2 | 3 | export default class List { 4 | private element: JQuery; 5 | 6 | constructor() { 7 | this.element = $('
    '); 8 | } 9 | 10 | public prepend(listItem: ListItem) { 11 | this.element.prepend(listItem.getDOM()); 12 | } 13 | 14 | public append(listItem: ListItem) { 15 | this.element.append(listItem.getDOM()); 16 | } 17 | 18 | public getDOM(): JQuery { 19 | return this.element; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /images/filetypes/text-code.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/connection/services/AbstractService.ts: -------------------------------------------------------------------------------- 1 | import { IConnection } from '@connection/Connection.interface'; 2 | import Account from '@src/Account'; 3 | 4 | type Send = (stanzaElement: Element | Strophe.Builder) => void; 5 | type SendIQ = (stanzaElement: Element | Strophe.Builder) => Promise; 6 | 7 | export default abstract class AbstractService { 8 | constructor(protected send: Send, protected sendIQ: SendIQ, 9 | protected connection: IConnection, protected account: Account) { 10 | 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /template/vcard.hbs: -------------------------------------------------------------------------------- 1 |

    {{t "Info_about"}} {{name}} ({{jid}})

    2 |
      3 | {{#each basic}} 4 |
    • 5 | {{t "Resource"}} {{resource}} 6 |
    • 7 |
    • 8 | {{t "Client"}} {{client}} 9 |
    • 10 |
    • 11 | {{t "Status"}} {{presence}} 12 |
    • 13 | {{/each}} 14 |
    15 | 16 |
    17 | {{t "Please_wait"}} 18 |
    19 | -------------------------------------------------------------------------------- /src/ui/util/DateTime.ts: -------------------------------------------------------------------------------- 1 | import * as moment from 'moment' 2 | 3 | export default class DateTime { 4 | public static stringify(stamp: number, elements?: JQuery) { 5 | let momentObject = moment(stamp); 6 | let fromNow = momentObject.fromNow(); 7 | 8 | if (!elements) { 9 | return fromNow 10 | } 11 | 12 | elements.each(function() { 13 | $(this).text(fromNow); 14 | }); 15 | 16 | setTimeout(function() { 17 | DateTime.stringify(stamp, elements); 18 | }, 1000 * 60); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /test/AccountStub.ts: -------------------------------------------------------------------------------- 1 | import JID from '../src/JID' 2 | 3 | export default class Account { 4 | 5 | public getJID(): JID { 6 | return new JID('foo@bar') 7 | } 8 | 9 | public getContact() { 10 | return { 11 | setStatus: (status) => { }, 12 | setPresence: (resource, status) => { }, 13 | setResource: (resource) => { } 14 | } 15 | } 16 | 17 | public getConnection() { 18 | return { 19 | sendSubscriptionAnswer: (jid, response) => { } 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /template/roster-form.hbs: -------------------------------------------------------------------------------- 1 |
    2 | 3 | 4 | 5 | 6 |
    {{t "Sorry_we_cant_authentikate_"}}
    7 | 8 | 9 |
    10 | -------------------------------------------------------------------------------- /images/filetypes/folder-drag-accept.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/bootstrap/webpackPublicPath.ts: -------------------------------------------------------------------------------- 1 | let scriptElements = document.querySelectorAll('script[src$="' + __BUNDLE_NAME__ + '"]'); 2 | if (scriptElements.length) { 3 | let src = ( scriptElements[0]).src; 4 | __webpack_public_path__ = src.substr(0, src.lastIndexOf('/') + 1); 5 | } else if (typeof ( window).jsxc_public_path === 'string') { 6 | __webpack_public_path__ = ( window).jsxc_public_path; 7 | } else { 8 | console.warn('Could not find script element which points to ' + __BUNDLE_NAME__ + '. JSXC is maybe not working correctly.'); 9 | } 10 | -------------------------------------------------------------------------------- /.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "stylelint-config-sass-guidelines", 3 | "rules": { 4 | "string-quotes": "double", 5 | "selector-max-id": 1, 6 | "max-nesting-depth": 5, 7 | "selector-max-compound-selectors": 5, 8 | "selector-no-qualifying-type": [true, { 9 | "ignore": ["attribute", "class", "id"] 10 | }], 11 | "selector-class-pattern": [ 12 | "^[a-z0-9_\\-]+$", 13 | { 14 | "message": 15 | "Selector should be written in lowercase with hyphens and underscores (selector-class-pattern)" 16 | } 17 | ] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/api/v1/disconnect.ts: -------------------------------------------------------------------------------- 1 | import Client from '@src/Client'; 2 | import { Presence } from '@connection/AbstractConnection'; 3 | 4 | export function disconnect() { 5 | return new Promise(resolve => { 6 | Client.getPresenceController().registerCurrentPresenceHook((presence) => { 7 | if (presence === Presence.offline) { 8 | resolve(); 9 | } 10 | }); 11 | 12 | if (Client.getAccountManager().getAccount()) { 13 | Client.getPresenceController().setTargetPresence(Presence.offline); 14 | } else { 15 | resolve(); 16 | } 17 | }); 18 | } 19 | -------------------------------------------------------------------------------- /src/Client.interface.ts: -------------------------------------------------------------------------------- 1 | import { IJID } from './JID.interface' 2 | 3 | export interface IClient { 4 | init(); 5 | 6 | addPreSendMessageHook(hook: (Message, Builder) => void, position?: number); 7 | 8 | hasFocus(); 9 | 10 | isExtraSmallDevice(): boolean; 11 | 12 | isDebugMode(): boolean; 13 | 14 | getStorage(); 15 | 16 | getAccount(jid: IJID): Account; 17 | getAccount(uid?: string): Account; 18 | 19 | createAccount(boshUrl: string, jid: string, sid: string, rid: string); 20 | createAccount(boshUrl: string, jid: string, password: string); 21 | 22 | removeAccount(account: Account); 23 | } 24 | -------------------------------------------------------------------------------- /images/filetypes/video.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /images/filetypes/x-office-presentation.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/connection/xmpp/namespace.ts: -------------------------------------------------------------------------------- 1 | import Log from '../../util/Log' 2 | import { Strophe } from '../../vendor/Strophe' 3 | 4 | let namespaces = {}; 5 | 6 | export function register(name: string, value: string): void { 7 | namespaces[name] = value; 8 | } 9 | 10 | export function get(name: string) { 11 | let value = Strophe.NS[name] || namespaces[name]; 12 | 13 | if (!value) { 14 | Log.warn('Can not resolve requested namespace ' + name); 15 | } 16 | 17 | return value; 18 | } 19 | 20 | export function getFilter(name: string, tagName: string = '') { 21 | return tagName + '[xmlns="' + get(name) + '"]'; 22 | } 23 | -------------------------------------------------------------------------------- /template/selection.hbs: -------------------------------------------------------------------------------- 1 | {{#if header}} 2 |

    {{header}}

    3 | {{/if}} 4 | 5 | {{#if message}} 6 |

    {{{breaklines message}}}

    7 | {{/if}} 8 | 9 | {{#if hasPrimary}} 10 | 17 | {{/if}} 18 | 19 | {{#if hasOption}} 20 | 27 | {{/if}} 28 | -------------------------------------------------------------------------------- /src/plugins/omemo/model/EncryptedDeviceMessage.ts: -------------------------------------------------------------------------------- 1 | import Address from '../vendor/Address'; 2 | 3 | export interface ICiphertext { 4 | type: number, 5 | body: string, 6 | registrationId: number, 7 | } 8 | 9 | export default class { 10 | constructor(private address: Address, private ciphertext: ICiphertext) { 11 | 12 | } 13 | 14 | public getDeviceId(): number { 15 | return this.address.getDeviceId(); 16 | } 17 | 18 | public isPreKey(): boolean { 19 | return this.ciphertext.type === 3; 20 | } 21 | 22 | public getCiphertext(): ICiphertext { 23 | return this.ciphertext; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /images/filetypes/package-x-generic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /template/fingerprints.hbs: -------------------------------------------------------------------------------- 1 |
    2 |

    {{t "A_fingerprint_"}}

    3 |

    4 | {{t "Your_fingerprint"}} 5 |
    6 | {{#if ownFingerprint}} 7 | {{ownFingerprint}} 8 | {{else}} 9 | {{t "not_available"}} 10 | {{/if}} 11 |

    12 |

    13 | 14 |
    15 | {{#if theirFingerprint}} 16 | {{theirFingerprint}} 17 | {{else}} 18 | {{t "not_available"}} 19 | {{/if}} 20 |

    21 |
    22 | -------------------------------------------------------------------------------- /images/hang_up_black.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/ui/dialogs/about.ts: -------------------------------------------------------------------------------- 1 | import Dialog from '../Dialog' 2 | import showDebugLog from './debugLog' 3 | import Client from '../../Client' 4 | 5 | const aboutTemplate = require('../../../template/about.hbs'); 6 | 7 | export default function() { 8 | let content = aboutTemplate({ 9 | version: __VERSION__, 10 | date: __BUILD_DATE__, 11 | appName: Client.getOption('appName'), 12 | dependencies: __DEPENDENCIES__, 13 | }); 14 | 15 | let dialog = new Dialog(content); 16 | let dom = dialog.open(); 17 | 18 | dom.find('.jsxc-debug-log').click(() => { 19 | dialog.close(); 20 | 21 | showDebugLog(); 22 | }); 23 | } 24 | -------------------------------------------------------------------------------- /.commitlintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@commitlint/config-conventional"], 3 | "rules": { 4 | "type-enum": [ 5 | 2, 6 | "always", 7 | [ 8 | "build", 9 | "ci", 10 | "chore", 11 | "docs", 12 | "feat", 13 | "fix", 14 | "perf", 15 | "refactor", 16 | "revert", 17 | "style", 18 | "test", 19 | "example", 20 | "release" 21 | ] 22 | ], 23 | "body-max-line-length": [ 24 | 1, 25 | "always", 26 | 100 27 | ] 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /images/hang_up_red.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/hang_up_white.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/ContactProvider.ts: -------------------------------------------------------------------------------- 1 | import { IContact } from './Contact.interface'; 2 | import { IJID } from './JID.interface'; 3 | import ContactManager from './ContactManager'; 4 | 5 | export default abstract class ContactProvider { 6 | public abstract getUid(): string 7 | 8 | public abstract load(): Promise 9 | 10 | public abstract add(contact: IContact): Promise 11 | 12 | public abstract createContact(jid: IJID, name?: string): IContact 13 | public abstract createContact(id: string): IContact 14 | 15 | public abstract deleteContact(jid: IJID): Promise 16 | 17 | constructor(protected contactManager: ContactManager) { 18 | 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /images/filetypes/folder.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /images/filetypes/folder-starred.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /images/pick_up_black.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "ciphertext", 4 | "jsxc", 5 | "omemo", 6 | "xmlns", 7 | "XMPP", 8 | "IJID", 9 | "JID" 10 | ], 11 | "todohighlight.include": [ 12 | "src/**/*.ts" 13 | ], 14 | "todohighlight.keywords": [{ 15 | "text": "@TODO", 16 | "backgroundColor": "blue", 17 | "color": "white" 18 | }, { 19 | "text": "@REVIEW", 20 | "backgroundColor": "green", 21 | "color": "white" 22 | }, { 23 | "text": "@FIX", 24 | "backgroundColor": "red", 25 | "color": "white" 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /images/pick_up_green.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/pick_up_white.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | 12 | 13 | **Is your feature request related to a specific XEP?** 14 | 15 | 16 | **Describe the solution you'd like** 17 | 18 | 19 | **Additional context** 20 | 21 | -------------------------------------------------------------------------------- /images/microphone_black.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/attachment.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/filetypes/file.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /images/microphone_white.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/filetypes/text.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /scss/main.scss: -------------------------------------------------------------------------------- 1 | .jsxc-window-list, 2 | .jsxc-window-list-handler, 3 | .jsxc-dialog, 4 | #jsxc-roster { 5 | * { 6 | box-sizing: border-box; 7 | } 8 | } 9 | 10 | @import "modules/all"; 11 | @import "vendor/all"; 12 | 13 | //fonts 14 | $font-sans: Arial, sans-serif; 15 | 16 | $default-window-width: 250px; 17 | 18 | @import "partials/avatar"; 19 | @import "partials/button"; 20 | @import "partials/bar"; 21 | @import "partials/dialog"; 22 | @import "partials/emoticons"; 23 | @import "partials/icon"; 24 | @import "partials/menu"; 25 | @import "partials/muc"; 26 | @import "partials/roster"; 27 | @import "partials/window-list"; 28 | @import "partials/window"; 29 | 30 | @import "partials/jsxc"; 31 | @import "partials/layout"; 32 | @import "partials/webrtc"; 33 | -------------------------------------------------------------------------------- /src/plugins/omemo/model/PreKey.ts: -------------------------------------------------------------------------------- 1 | import IExportable from './Exportable'; 2 | 3 | export interface IPreKeyObject { 4 | keyId: number 5 | keyPair: { 6 | publicKey: ArrayBuffer, 7 | privateKey?: ArrayBuffer, 8 | } 9 | } 10 | 11 | export default class PreKey implements IExportable { 12 | constructor(protected data: IPreKeyObject) { 13 | 14 | } 15 | 16 | public getId(): number { 17 | return this.data.keyId; 18 | } 19 | 20 | public getPublic(): ArrayBuffer { 21 | return this.data.keyPair.publicKey; 22 | } 23 | 24 | public getPrivate(): ArrayBuffer | undefined { 25 | return this.data.keyPair.privateKey; 26 | } 27 | 28 | public export(): IPreKeyObject { 29 | return this.data; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /images/filetypes/image.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /images/filetypes/x-office-document.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /images/camera_disabled_icon_black.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /scss/modules/_presence.scss: -------------------------------------------------------------------------------- 1 | $presences: online chat away xa dnd; 2 | 3 | @mixin presence-indicator($target) { 4 | 5 | @each $presence in $presences { 6 | [data-presence="#{$presence}"]#{$target} { 7 | &::before { 8 | background-color: map-get($presence-colors, $presence); 9 | background-image: url("../images/presence/#{$presence}.svg"); 10 | background-position: center; 11 | background-repeat: no-repeat; 12 | background-size: 100%; 13 | border-radius: 100%; 14 | box-sizing: content-box; 15 | color: #fff; 16 | content: ""; 17 | display: block; 18 | height: 12px; 19 | line-height: 12px; 20 | text-align: center; 21 | width: 12px; 22 | z-index: 99; 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/ui/DialogNavigation.ts: -------------------------------------------------------------------------------- 1 | import Page from './DialogPage' 2 | 3 | export default class Navigation { 4 | private history: JQuery[] = []; 5 | 6 | constructor(private rootElement: JQuery) { 7 | 8 | } 9 | 10 | public goBack() { 11 | if (this.history.length === 1) { 12 | return; 13 | } 14 | 15 | this.history.shift().remove(); 16 | this.history[0].appendTo(this.rootElement); 17 | } 18 | 19 | public goTo(page: Page) { 20 | let currentPage = this.history[0]; 21 | 22 | if (currentPage) { 23 | currentPage.detach(); 24 | } 25 | 26 | this.history.unshift(page.getDOM()); 27 | this.history[0].appendTo(this.rootElement); 28 | } 29 | 30 | public canGoBack(): boolean { 31 | return this.history.length > 0; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /images/dragover_white.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/drop_white.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/ui/DialogSection.ts: -------------------------------------------------------------------------------- 1 | import Navigation from './DialogNavigation' 2 | 3 | export default abstract class Section { 4 | private element: JQuery; 5 | 6 | constructor(protected navigation: Navigation, private title?: string) { 7 | 8 | } 9 | 10 | public getDOM(): JQuery { 11 | if (!this.element) { 12 | this.generateDOM(); 13 | } 14 | 15 | return this.element; 16 | } 17 | 18 | private generateDOM() { 19 | this.element = $('
    '); 20 | 21 | if (this.title) { 22 | let legendElement = $('

    '); 23 | legendElement.text(this.title); 24 | legendElement.appendTo(this.element); 25 | } 26 | 27 | this.element.append(this.generateContentElement()); 28 | } 29 | 30 | protected abstract generateContentElement(): JQuery 31 | } 32 | -------------------------------------------------------------------------------- /images/camera_disabled_icon_white.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/filetypes/x-office-spreadsheet.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /images/padlock_close_green.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/util/UUID.ts: -------------------------------------------------------------------------------- 1 | export default class UUID { 2 | public static v4(): string { 3 | if (crypto && typeof crypto.getRandomValues === 'function') { 4 | return UUID.v4withCSPRG(); 5 | } else { 6 | return UUID.v4withoutCSPRG(); 7 | } 8 | } 9 | 10 | private static v4withCSPRG(): string { 11 | return `${1e7}-${1e3}-${4e3}-${8e3}-${1e11}`.replace(/[018]/g, c => 12 | (parseInt(c, 10) ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> parseInt(c, 10) / 4).toString(16) 13 | ) 14 | } 15 | 16 | private static v4withoutCSPRG(): string { 17 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { 18 | let r = Math.random() * 16 | 0; 19 | let v = c === 'x' ? r : (r & 0x3 | 0x8); 20 | return v.toString(16); 21 | }); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /images/filetypes/folder-external.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /images/padlock_close_orange.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /template/dialogOmemoDeviceItem.hbs: -------------------------------------------------------------------------------- 1 |
    2 | {{#unless isCurrentDevice}} 3 |
    4 | {{/unless}} 5 |
    {{id}}{{#if isCurrentDevice}} ({{t "this_device"}}){{/if}}
    6 |
    {{fingerprint}}
    7 |
    {{trust}}
    8 |
    9 | {{#if showControls}} 10 | {{t "Ignore"}}  11 | {{t "Recognize"}}  12 | {{t "Verify"}} 13 | {{/if}} 14 |
    15 |
    16 | -------------------------------------------------------------------------------- /custom.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.hbs' { 2 | // const content: any; 3 | // export default content; 4 | export default function template(options: any); 5 | } 6 | 7 | declare module '*.png' { 8 | const content: string; 9 | export default content; 10 | } 11 | declare module '*.mp3' { 12 | const content: string; 13 | export default content; 14 | } 15 | declare module '*.wav' { 16 | const content: string; 17 | export default content; 18 | } 19 | declare module '*.js?path' { } 20 | declare module '*.svg' { 21 | const content: string; 22 | export default content; 23 | } 24 | 25 | declare var __webpack_public_path__: string; 26 | declare var __VERSION__: string; 27 | declare var __BUILD_DATE__: string; 28 | declare var __BUNDLE_NAME__: string; 29 | declare var __DEPENDENCIES__: string; 30 | declare let __LANGS__: string[]; 31 | -------------------------------------------------------------------------------- /images/filetypes/audio.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/ui/dialogs/multiUserInvite.ts: -------------------------------------------------------------------------------- 1 | import Dialog from '../Dialog' 2 | import MultiUserContact from '../../MultiUserContact' 3 | import JID from '../../JID' 4 | 5 | let multiUserInvite = require('../../../template/multiUserInvite.hbs'); 6 | 7 | export default function(multiUserContact: MultiUserContact) { 8 | let content = multiUserInvite({}); 9 | 10 | let dialog = new Dialog(content); 11 | let dom = dialog.open(); 12 | 13 | //@TODO add datalist of all jids in roster 14 | 15 | dom.find('form').on('submit', (ev) => { 16 | ev.preventDefault(); 17 | 18 | let reason = dom.find('input[name="reason"]').val(); 19 | let jidString = dom.find('input[name="jid"]').val(); 20 | let jid = new JID(jidString); 21 | 22 | multiUserContact.invite(jid, reason); 23 | 24 | dialog.close(); 25 | }) 26 | } 27 | -------------------------------------------------------------------------------- /scss/modules/_scrollbar.scss: -------------------------------------------------------------------------------- 1 | %scrollbar { 2 | &::-webkit-scrollbar { 3 | height: 2px; 4 | width: 2px; 5 | } 6 | 7 | &::-webkit-scrollbar-button { 8 | height: 0; 9 | width: 0; 10 | } 11 | 12 | &::-webkit-scrollbar-thumb { 13 | background: #d1d1d1; 14 | border: 0; 15 | border-radius: 1px; 16 | border-right: 3px solid transparent; 17 | 18 | &:hover { 19 | background: #c1c1c1; 20 | } 21 | 22 | &:active { 23 | background: #b1b1b1; 24 | } 25 | } 26 | 27 | &::-webkit-scrollbar-track { 28 | background: transparent; 29 | border: 0 none #fff; 30 | border-radius: 50px; 31 | 32 | &:hover { 33 | background: transparent; 34 | } 35 | 36 | &:active { 37 | background: transparent; 38 | } 39 | } 40 | 41 | &::-webkit-scrollbar-corner { 42 | background: transparent; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/ui/dialogs/confirm.ts: -------------------------------------------------------------------------------- 1 | import Dialog from '../Dialog' 2 | import Utils from '@util/Utils'; 3 | 4 | let confirmTemplate = require('../../../template/confirm.hbs'); 5 | 6 | export default function(question: string, safeContent: boolean = false) { 7 | if (safeContent !== true) { 8 | question = Utils.escapeHTML(question); 9 | } 10 | 11 | let content = confirmTemplate({ 12 | question 13 | }); 14 | 15 | let dialog = new Dialog(content); 16 | let dom = dialog.open(); 17 | 18 | let promise = new Promise((resolve, reject) => { 19 | dom.find('.jsxc-confirm').click(function() { 20 | resolve(dialog); 21 | }); 22 | 23 | dom.find('.jsxc-dismiss').click(function() { 24 | reject(dialog); 25 | }); 26 | }); 27 | 28 | dialog.getPromise = () => { 29 | return promise; 30 | }; 31 | 32 | return dialog; 33 | } 34 | -------------------------------------------------------------------------------- /src/plugins/chatState/ChatStateConnection.ts: -------------------------------------------------------------------------------- 1 | import * as Namespace from '../../connection/xmpp/namespace' 2 | import JID from '../../JID' 3 | import { STATE } from './State' 4 | import { $msg } from '../../vendor/Strophe' 5 | 6 | export default class ChatStateConnection { 7 | constructor(private send) { 8 | 9 | } 10 | 11 | public sendPaused(to: JID, type: 'chat' | 'groupchat' = 'chat') { 12 | this.sendState(STATE.PAUSED, to, type); 13 | } 14 | 15 | public sendComposing(to: JID, type: 'chat' | 'groupchat' = 'chat') { 16 | this.sendState(STATE.COMPOSING, to, type); 17 | } 18 | 19 | private sendState(state: STATE, to: JID, type: 'chat' | 'groupchat' = 'chat') { 20 | let msg = $msg({ 21 | to: to.full, 22 | type 23 | }).c(state, { 24 | xmlns: Namespace.get('CHATSTATES') 25 | }); 26 | 27 | this.send(msg); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/vendor/Strophe.ts: -------------------------------------------------------------------------------- 1 | import * as StropheLib from 'strophe.js' 2 | 3 | export let $iq: (attrs?: any) => Strophe.Builder = StropheLib.$iq; 4 | export let $build: (name: string, attrs?: any) => Strophe.Builder = StropheLib.$build; 5 | export let $msg: (attrs?: any) => Strophe.Builder = StropheLib.$msg; 6 | export let $pres: (attrs?: any) => Strophe.Builder = StropheLib.$pres; 7 | export let Strophe = StropheLib.Strophe; 8 | export let NS = StropheLib.Strophe.NS; 9 | export let Status: IStatus = StropheLib.Strophe.Status; 10 | 11 | interface IStatus { 12 | ATTACHED: number 13 | AUTHENTICATING: number 14 | AUTHFAIL: number 15 | CONNECTED: number 16 | CONNECTING: number 17 | CONNFAIL: number 18 | CONNTIMEOUT: number 19 | DISCONNECTED: number 20 | DISCONNECTING: number 21 | ERROR: number 22 | REDIRECT: number 23 | } 24 | 25 | ( window).Strophe = Strophe; 26 | ( window).$iq = $iq; 27 | -------------------------------------------------------------------------------- /template/roster-item.hbs: -------------------------------------------------------------------------------- 1 |
  • 2 |
    3 | 4 |
    5 |
    {{name}}
    6 |
    {{lastMessage}}
    7 |
    8 | 9 | {{#> menu classes="jsxc-bar__action-entry jsxc-menu--vertical-left jsxc-menu--light" button-classes="jsxc-icon jsxc-icon--more-dark jsxc-icon--clickable"}} 10 |
  • 11 |
  • 12 |
  • 13 | {{/menu}} 14 | 15 | -------------------------------------------------------------------------------- /test/connection/xmpp/handlers/presence.spec.ts: -------------------------------------------------------------------------------- 1 | import 'mocha'; 2 | 3 | describe('Presence handler', () => { 4 | before(function() { 5 | // let account = new AccountStub; 6 | // let getAccountStub = sinon.stub(Client, 'getAccount').returns(account); 7 | // handler = new PresenceHandler(getAccountStub()); 8 | 9 | // let logStub = sinon.stub(Log, 'log').callsFake(function(level, message:string, data?:any){ 10 | 11 | // }); 12 | }); 13 | 14 | it('should ignore own presence notification'); 15 | 16 | it('should abort if stanza is of type "error"') 17 | 18 | it('should process a subscription request'); 19 | 20 | it('should set text status for contact'); 21 | 22 | it('should set presence for resource and not change contact presence'); 23 | 24 | it('should set presence for resource and change contact presence'); 25 | 26 | it('should reset ressource'); 27 | }); 28 | -------------------------------------------------------------------------------- /src/plugins/omemo/model/IdentityKey.ts: -------------------------------------------------------------------------------- 1 | import ArrayBufferUtils from '../util/ArrayBuffer' 2 | import IExportable from './Exportable'; 3 | 4 | interface IIdentityKeyObject { publicKey: ArrayBuffer, privateKey?: ArrayBuffer } 5 | 6 | export default class IdentityKey implements IExportable { 7 | constructor(private data: IIdentityKeyObject) { 8 | 9 | } 10 | 11 | public getFingerprint(): string { 12 | return this.data.publicKey ? ArrayBufferUtils.toPrettyHex(this.getPublicKeyWithoutVersionByte()) : ''; 13 | } 14 | 15 | public getPublic(): ArrayBuffer { 16 | return this.data.publicKey; 17 | } 18 | 19 | public getPrivate(): ArrayBuffer { 20 | return this.data.privateKey; 21 | } 22 | 23 | private getPublicKeyWithoutVersionByte(): ArrayBuffer { 24 | return this.data.publicKey.slice(1); 25 | } 26 | 27 | public export(): IIdentityKeyObject { 28 | return this.data; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/plugins/omemo/vendor/Address.ts: -------------------------------------------------------------------------------- 1 | import { SignalAddress } from './Signal'; 2 | 3 | export default class Address { 4 | private address; 5 | 6 | public static fromString(text: string): Address { 7 | let matches = text.match(/^(.+?)(?:\.(\d+))?$/); 8 | let name = matches[1]; 9 | let deviceId = matches[2]; 10 | 11 | return new SignalAddress(name, deviceId); 12 | } 13 | 14 | constructor(name: string, deviceId: number) { 15 | this.address = new SignalAddress(name, deviceId); 16 | } 17 | 18 | public getName(): string { 19 | return this.address.getName(); 20 | } 21 | 22 | public getDeviceId(): number { 23 | return this.address.getDeviceId(); 24 | } 25 | 26 | public toString(): string { 27 | return this.address.toString(); 28 | } 29 | 30 | public equals(address: Address): boolean { 31 | return this.address.toString() === address.toString(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /images/padlock_open_black.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /scss/partials/_emoticons.scss: -------------------------------------------------------------------------------- 1 | $emoticons: jabber, jsxc, klaus, nextcloud, xmpp, owncloud; 2 | 3 | [class*=jsxc-emoticon-] { //@REVIEW vs jsxc-emoticon 4 | background-position: center center; 5 | background-repeat: no-repeat; 6 | background-size: 1em 1em; 7 | display: inline-block; 8 | height: 1em; 9 | line-height: normal; 10 | margin: -0.2ex 0.05em 0 0.1em; 11 | vertical-align: middle; 12 | width: 1em; 13 | } 14 | 15 | .jsxc-emoticon { 16 | background-size: contain; 17 | border: 0; 18 | display: inline-block; 19 | height: 19px; 20 | vertical-align: bottom; 21 | width: 19px; 22 | 23 | &--large { 24 | height: 40px; 25 | margin-bottom: 7px; 26 | width: 40px; 27 | 28 | #jsxc-roster & { 29 | height: 19px; 30 | width: 19px; 31 | } 32 | } 33 | 34 | @each $emoticon in $emoticons { 35 | &--#{$emoticon} { 36 | background-image: url("../images/emoticons/#{$emoticon}.svg"); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/DiscoInfoRepository.interface.ts: -------------------------------------------------------------------------------- 1 | import { IContact as Contact } from './Contact.interface' 2 | import { IJID as JID } from './JID.interface' 3 | import { IDiscoInfo } from './DiscoInfo.interface' 4 | 5 | export interface IDiscoInfoRepository { 6 | addRelation(jid: JID, version: string) 7 | addRelation(jid: JID, discoInfo: IDiscoInfo) 8 | 9 | getDiscoInfo(jid: JID) 10 | 11 | getCapableResources(contact: Contact, features: string[]): Promise 12 | getCapableResources(contact: Contact, features: string): Promise 13 | 14 | hasFeature(jid: JID, features: string[]): Promise 15 | hasFeature(jid: JID, feature: string): Promise 16 | hasFeature(discoInfo: IDiscoInfo, features: string[]): Promise 17 | hasFeature(discoInfo: IDiscoInfo, feature: string): Promise 18 | 19 | getCapabilities(jid: JID): Promise 20 | 21 | requestDiscoInfo(jid: JID, node?: string) 22 | } 23 | -------------------------------------------------------------------------------- /images/padlock_open_white.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/util/PersistentArray.ts: -------------------------------------------------------------------------------- 1 | import Storage from '../Storage' 2 | 3 | export default class PersistentArray { 4 | 5 | private array: any[]; 6 | 7 | private key: string; 8 | 9 | constructor(private storage: Storage, ...identifier: string[]) { 10 | this.key = storage.generateKey.apply(storage, identifier); 11 | 12 | this.array = this.storage.getItem(this.key) || []; 13 | 14 | this.storage.registerHook(this.key, (newValue) => { 15 | this.array = newValue; 16 | }); 17 | } 18 | 19 | public push(element) { 20 | this.array.push(element); 21 | 22 | this.save(); 23 | } 24 | 25 | public pop(): any { 26 | let element = this.array.pop(); 27 | 28 | this.save(); 29 | 30 | return element; 31 | } 32 | 33 | public indexOf(element): number { 34 | return this.array.indexOf(element); 35 | } 36 | 37 | private save() { 38 | this.storage.setItem(this.key, this.array); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /images/filetypes/text-calendar.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/plugins/omemo/lib/IdentityManager.ts: -------------------------------------------------------------------------------- 1 | import Store from './Store'; 2 | import Address from '../vendor/Address'; 3 | import BundleManager from './BundleManager'; 4 | import IdentityKey from '../model/IdentityKey'; 5 | 6 | export default class { 7 | constructor(private store: Store, private bundleManager: BundleManager) { 8 | 9 | } 10 | 11 | public async loadFingerprint(identifier: Address): Promise { 12 | let identityKey = await this.loadIdentityKey(identifier); 13 | 14 | this.store.saveIdentity(identifier, identityKey); 15 | 16 | return identityKey.getFingerprint(); 17 | } 18 | 19 | public async loadIdentityKey(identifier: Address): Promise { 20 | let identityKey = this.store.getIdentityKey(identifier); 21 | 22 | if (identityKey) { 23 | return identityKey; 24 | } 25 | 26 | let bundle = await this.bundleManager.requestBundle(identifier); 27 | 28 | return bundle.getIdentityKey(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /template/videoDialog.hbs: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
      4 |
      5 |
      6 | 7 | 8 |
      9 | 10 | {{!--
      11 |
      12 |
      13 |

      14 |
      15 |
      16 |
      --}} 17 | 18 |
      19 |
      20 |
      21 |
      22 |
      23 |
      24 |
      25 |
      26 |
      27 |
      28 |
      29 | -------------------------------------------------------------------------------- /src/PageVisibility.ts: -------------------------------------------------------------------------------- 1 | import Client from './Client' 2 | 3 | export default class PageVisibility { 4 | private static isInitialized = false; 5 | 6 | public static init() { 7 | if (PageVisibility.isInitialized) { 8 | return; 9 | } 10 | 11 | PageVisibility.isInitialized = true; 12 | 13 | Client.getStorage().registerHook('isHidden', (hidden) => { 14 | if (hidden && !document.hidden) { 15 | Client.getStorage().setItem('isHidden', document.hidden); 16 | } 17 | }) 18 | 19 | function visibilityChangeHandler() { 20 | Client.getStorage().setItem('isHidden', document.hidden); 21 | } 22 | 23 | $(document).on('visibilitychange', visibilityChangeHandler); 24 | visibilityChangeHandler(); 25 | } 26 | 27 | public static isHidden() { 28 | return Client.getStorage().getItem('isHidden'); 29 | } 30 | 31 | public static isVisible() { 32 | return !PageVisibility.isHidden(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /images/minimize_black.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/Storage.interface.ts: -------------------------------------------------------------------------------- 1 | export default interface IStorage { 2 | 3 | getName(): string 4 | generateKey(...args: string[]): string 5 | getPrefix(): string 6 | getBackend() 7 | 8 | setItem(type: string, key: string, value: any): void; 9 | setItem(key: string, value: any): void 10 | 11 | getItem(type: string, key: string): any; 12 | getItem(key: string): any; 13 | 14 | removeItem(type, key): void; 15 | removeItem(key): void; 16 | 17 | updateItem(type, key, variable, value): void; 18 | updateItem(key, variable, value): void; 19 | 20 | increment(key: string): void 21 | 22 | removeElement(type, key, name): void; 23 | removeElement(key, name): void; 24 | 25 | getItemsWithKeyPrefix(keyPrefix: string) 26 | 27 | registerHook(eventName: string, func: (newValue: any, oldValue: any, key: string) => void) 28 | 29 | removeHook(eventName: string, func?: (newValue: any, oldValue: any, key: string) => void) 30 | 31 | removeAllHooks() 32 | 33 | destroy() 34 | } 35 | -------------------------------------------------------------------------------- /src/util/Random.ts: -------------------------------------------------------------------------------- 1 | import Log from './Log' 2 | 3 | export default class Random { 4 | public static number(max: number, min: number = 0): number { 5 | if (crypto && typeof crypto.getRandomValues === 'function') { 6 | return Random.numberWithCSPRG(max, min); 7 | } else { 8 | return Random.numberWithoutCSPRG(max, min); 9 | } 10 | } 11 | 12 | private static numberWithCSPRG(max: number, min: number): number { 13 | const randomBuffer = new Uint32Array(1); 14 | 15 | window.crypto.getRandomValues(randomBuffer); 16 | 17 | let randomNumber = randomBuffer[0] / (0xffffffff + 1); 18 | 19 | min = Math.ceil(min); 20 | max = Math.floor(max); 21 | return Math.floor(randomNumber * (max - min + 1)) + min; 22 | } 23 | 24 | private static numberWithoutCSPRG(max: number, min: number): number { 25 | Log.warn('Random number is generated without CSPRG'); 26 | 27 | return Math.floor(Math.random() * (max - min)) + min; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/ui/util/ElementHandler.ts: -------------------------------------------------------------------------------- 1 | import Contact from '../../Contact' 2 | 3 | const CLASS_DISABLED = 'jsxc-disabled'; 4 | 5 | export default class ElementHandler { 6 | constructor(private contact: Contact) { 7 | 8 | } 9 | 10 | public add(element: Element, handler: (ev: Event) => void, requiredFeatures?: string[]) { 11 | if (requiredFeatures && requiredFeatures.length > 0) { 12 | this.contact.registerCapableResourcesHook(requiredFeatures, (resources) => { 13 | this.updateStatus(element, resources); 14 | }); 15 | } 16 | 17 | $(element).on('click', function() { 18 | if (!element.classList.contains(CLASS_DISABLED)) { 19 | handler.apply(this, arguments); 20 | } 21 | }) 22 | } 23 | 24 | private updateStatus(element, resources) { 25 | if (resources && resources.length > 0) { 26 | element.classList.remove(CLASS_DISABLED); 27 | } else { 28 | element.classList.add(CLASS_DISABLED); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /images/contact_black.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/contact_white.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/plugins/bookmarks/RoomBookmark.ts: -------------------------------------------------------------------------------- 1 | import { IJID } from '@src/JID.interface'; 2 | 3 | export default class RoomBookmark { 4 | constructor(private id: IJID, private alias?: string, private nickname?: string, private autoJoin: boolean = false, private password?: string) { 5 | 6 | } 7 | 8 | public getId(): string { 9 | return this.id.bare; 10 | } 11 | 12 | public getJid(): IJID { 13 | return this.id; 14 | } 15 | 16 | public hasAlias(): boolean { 17 | return !!this.alias; 18 | } 19 | 20 | public getAlias(): string { 21 | return this.alias; 22 | } 23 | 24 | public hasNickname(): boolean { 25 | return !!this.nickname; 26 | } 27 | 28 | public getNickname(): string { 29 | return this.nickname; 30 | } 31 | 32 | public hasPassword(): boolean { 33 | return !!this.password; 34 | } 35 | 36 | public getPassword(): string { 37 | return this.password; 38 | } 39 | 40 | public isAutoJoin(): boolean { 41 | return this.autoJoin; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/ui/dialogs/unknownSender.ts: -------------------------------------------------------------------------------- 1 | import SelectionDialog from './selection'; 2 | import Translation from '@util/Translation'; 3 | import Contact from '@src/Contact'; 4 | import JID from '@src/JID'; 5 | import Client from '@src/Client'; 6 | 7 | export default function(accountId: string, header: string, description: string, fromString: string) { 8 | SelectionDialog({ 9 | header, 10 | message: description, 11 | primary: { 12 | label: Translation.t('Open_window'), 13 | cb: () => { 14 | let jid = new JID(fromString); 15 | let account = Client.getAccountManager().getAccount(accountId); 16 | 17 | if (!account) { 18 | return; 19 | } 20 | 21 | let contact = new Contact(account, jid); 22 | 23 | account.getContactManager().addToCache(contact); 24 | 25 | contact.getChatWindowController().openProminently(); 26 | } 27 | }, 28 | option: { 29 | cb: () => undefined, 30 | } 31 | }); 32 | } 33 | -------------------------------------------------------------------------------- /src/ui/util/TableElement.ts: -------------------------------------------------------------------------------- 1 | import Log from '@util/Log'; 2 | 3 | export default class TableElement { 4 | private tableElement = $(''); 5 | 6 | constructor(private numberOfColumns: number) { 7 | 8 | } 9 | 10 | public appendRow(...columns) { 11 | return this.addRow('appendTo', columns); 12 | } 13 | 14 | public prependRow(...columns) { 15 | return this.addRow('prependTo', columns); 16 | } 17 | 18 | public get() { 19 | return this.tableElement; 20 | } 21 | 22 | private addRow(position: 'appendTo' | 'prependTo', columns) { 23 | if (columns.length !== this.numberOfColumns) { 24 | Log.warn('Wrong number of columns'); 25 | 26 | return false; 27 | } 28 | 29 | let rowElement = $(''); 30 | 31 | for (let column of columns) { 32 | let cellElement = $('
      '); 33 | 34 | cellElement.text(column); 35 | 36 | cellElement.appendTo(rowElement); 37 | } 38 | 39 | rowElement[position](this.tableElement); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /template/about.hbs: -------------------------------------------------------------------------------- 1 |

      JavaScript XMPP Chat

      2 |

      3 | Version: {{version}}
      4 | Build date: {{date}}
      5 | www.jsxc.org 6 |

      7 |

      8 | Released under the MIT license 9 |

      10 |

      11 | Real-time chat app for {{appName}} and more. 12 |

      13 |

      14 | {{t "Credits"}}: David English (Ringtone), 15 | CameronMusic (Ping), 16 | Picol (Fullscreen icon), Jabber Software Foundation (Jabber lightbulb logo) 17 |

      18 |

      19 | {{t "Libraries"}}: 20 | {{dependencies}} 21 |

      22 | 23 | 24 | -------------------------------------------------------------------------------- /template/multiUserInvite.hbs: -------------------------------------------------------------------------------- 1 |

      {{t "Invite_user"}}

      2 |

      {{t "muc_invite_explanation"}}

      3 |
      4 |
      5 | 6 |
      7 | 8 |
      9 |
      10 |
      11 | 12 |
      13 | 14 |
      15 |
      16 |
      17 |
      18 | 19 | 20 |
      21 |
      22 |
      23 | -------------------------------------------------------------------------------- /images/download_icon_black.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": [ 4 | "tslint:recommended" 5 | ], 6 | "jsRules": {}, 7 | "rules": { 8 | "quotemark": [true, "single"], 9 | "prefer-const": false, 10 | "trailing-comma": false, 11 | "semicolon": false, 12 | "ordered-imports": false, 13 | "member-ordering": false, 14 | "arrow-parens": false, 15 | "no-angle-bracket-type-assertion": false, 16 | "comment-format": false, 17 | "max-line-length": false, 18 | "only-arrow-functions": false, 19 | "unified-signatures": false, 20 | "interface-name": true, 21 | "object-literal-sort-keys": false, 22 | "max-classes-per-file": false, 23 | "no-console": false, 24 | "no-bitwise": false, 25 | "no-shadowed-variable": false, 26 | "no-var-requires": false, 27 | "no-unused-expression": [true, "allow-new"], 28 | "forin": false, 29 | "no-empty": false, 30 | "ban-types": false 31 | }, 32 | "rulesDirectory": [] 33 | } 34 | -------------------------------------------------------------------------------- /images/download_icon_white.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/plugins/omemo/vendor/SignalStore.interface.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface IIdentityKeyPair { privKey?: ArrayBuffer, pubKey: ArrayBuffer } 3 | export interface IPreKeyPair { pubKey, privKey } 4 | export interface ISignedPreKeyPair { signature, pubKey, privKey } 5 | 6 | interface ISignalStore { 7 | Direction: { SENDING: number, RECEIVING: number } 8 | 9 | getIdentityKeyPair(): Promise; 10 | 11 | getLocalRegistrationId(): Promise; 12 | 13 | isTrustedIdentity(addressName: string, identityKey: ArrayBuffer, direction: number): Promise; 14 | 15 | saveIdentity(address: string, identityKey: ArrayBuffer): Promise; 16 | 17 | loadPreKey(keyId: number): Promise; 18 | 19 | removePreKey(keyId: number): Promise; 20 | 21 | loadSignedPreKey(keyId: number): Promise; 22 | 23 | loadSession(address: string): Promise; 24 | 25 | storeSession(identifier: string, session: string): Promise; 26 | } 27 | 28 | export default ISignalStore; 29 | -------------------------------------------------------------------------------- /src/connection/xmpp/handlers/multiUser/Presence.ts: -------------------------------------------------------------------------------- 1 | import Log from '../../../../util/Log' 2 | import JID from '../../../../JID' 3 | import MultiUserContact from '../../../../MultiUserContact' 4 | import AbstractHandler from '../../AbstractHandler' 5 | import MultiUserPresenceProcessor from './PresenceProcessor' 6 | 7 | export default class extends AbstractHandler { 8 | public processStanza(stanza: Element): boolean { 9 | Log.debug('onMultiUserPresence', stanza); 10 | 11 | let from = new JID($(stanza).attr('from')); 12 | let type = $(stanza).attr('type'); 13 | 14 | let xElement = $(stanza).find('x[xmlns="http://jabber.org/protocol/muc#user"]'); 15 | let multiUserContact = this.account.getContact(from); 16 | 17 | if (!(multiUserContact instanceof MultiUserContact) || xElement.length === 0) { 18 | return this.PRESERVE_HANDLER; 19 | } 20 | 21 | let nickname = from.resource; 22 | 23 | new MultiUserPresenceProcessor(multiUserContact, xElement, nickname, type); 24 | 25 | return this.PRESERVE_HANDLER; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/plugins/omemo/vendor/SessionCipher.ts: -------------------------------------------------------------------------------- 1 | import { SignalSessionCipher, SignalAddress } from './Signal'; 2 | import Address from './Address'; 3 | import Store from '../lib/Store'; 4 | import { ICiphertext } from '../model/EncryptedDeviceMessage'; 5 | 6 | export class SessionCipher { 7 | private signalSessionCipher; 8 | 9 | constructor(address: Address, store: Store) { 10 | let signalAddress = new SignalAddress(address.getName(), address.getDeviceId()); 11 | 12 | this.signalSessionCipher = new SignalSessionCipher(store.getSignalStore(), signalAddress); 13 | } 14 | 15 | public decryptPreKeyMessage(ciphertext): Promise { 16 | return this.signalSessionCipher.decryptPreKeyWhisperMessage(ciphertext, 'binary'); 17 | } 18 | 19 | public decryptMessage(ciphertext): Promise { 20 | return this.signalSessionCipher.decryptWhisperMessage(ciphertext, 'binary'); 21 | } 22 | 23 | public encryptMessage(plaintext): Promise { 24 | return this.signalSessionCipher.encrypt(plaintext, 'binary'); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /images/megaphone_icon_black.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "allowSyntheticDefaultImports": true, 3 | "allowJs": true, 4 | "compilerOptions": { 5 | "baseUrl": "src", 6 | "paths": { 7 | "emojione": ["lib/emojione/lib/js/emojione.js"], 8 | "@connection/*": ["connection/*"], 9 | "@ui/*": ["ui/*"], 10 | "@util/*": ["util/*"], 11 | "@vendor/*": ["vendor/*"], 12 | "@src/*": ["./*"], 13 | "@interactjs/interactjs/*": ["../node_modules/@interactjs/interact/*"] 14 | }, 15 | "moduleResolution": "node", 16 | "module": "commonjs", 17 | "outDir": "./dist", 18 | "target": "es5", 19 | "lib": ["dom", "es2015", "es2015.promise", "es5"], 20 | "noUnusedLocals": true, 21 | "downlevelIteration": true 22 | }, 23 | "include": [ 24 | "./src/**/*.ts", 25 | "./template/*.hbs", 26 | "./*.d.ts", 27 | "./test/**/*.ts" 28 | ], 29 | "exclude": [ 30 | "node_modules", 31 | "typings" 32 | ], 33 | "files": [ 34 | "custom.d.ts" 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /src/ui/DialogPage.ts: -------------------------------------------------------------------------------- 1 | import Navigation from './DialogNavigation' 2 | 3 | export default abstract class Page { 4 | private element: JQuery; 5 | 6 | constructor(protected navigation: Navigation, private title: string) { 7 | 8 | } 9 | 10 | public getDOM(): JQuery { 11 | if (!this.element) { 12 | this.generateDOM(); 13 | } 14 | 15 | return this.element; 16 | } 17 | 18 | private generateDOM() { 19 | this.element = $('
      '); 20 | this.element.addClass('jsxc-page'); 21 | 22 | let legendElement = $('

      '); 23 | legendElement.addClass('jsxc-page__headline') 24 | legendElement.text(this.title); 25 | 26 | if (this.navigation.canGoBack()) { 27 | legendElement.addClass('jsxc-clickable'); //@REVIEW 28 | legendElement.on('click', () => { 29 | this.navigation.goBack(); 30 | }); 31 | } 32 | 33 | legendElement.appendTo(this.element); 34 | 35 | this.element.append(this.generateContentElement()); 36 | } 37 | 38 | protected abstract generateContentElement(): JQuery | JQuery[] 39 | } 40 | -------------------------------------------------------------------------------- /src/LinkHandlerGeo.ts: -------------------------------------------------------------------------------- 1 | import { ILinkHandler } from './LinkHandler.interface'; 2 | import Location from '@util/Location'; 3 | 4 | export default class LinkHandlerGeo implements ILinkHandler { 5 | private static instance: LinkHandlerGeo; 6 | 7 | public static get(): LinkHandlerGeo { 8 | if (!LinkHandlerGeo.instance) { 9 | LinkHandlerGeo.instance = new LinkHandlerGeo(); 10 | } 11 | 12 | return LinkHandlerGeo.instance; 13 | } 14 | 15 | public detect(element: JQuery) { 16 | element.find('[href^="geo:"]').each(function() { 17 | let uri = $(this).attr('href'); 18 | let coords = Location.parseGeoUri(uri); 19 | let link = Location.locationToLink(coords.latitude, coords.longitude); 20 | let label = 'OSM: ' + Location.ddToDms(coords.latitude, coords.longitude); 21 | 22 | if (coords.accuracy) { 23 | label += ' (±' + (Math.round(coords.accuracy * 10) / 10) + 'm)'; 24 | } 25 | 26 | $(this).attr('href', link); 27 | $(this).text(label); 28 | $(this).addClass('jsxc-geo'); 29 | }) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/CONST.ts: -------------------------------------------------------------------------------- 1 | import incomingMessageSoundFile from '../sound/incomingMessage.mp3'; 2 | import incomingCallSoundFile from '../sound/Rotary-Phone6.mp3'; 3 | import noticeSoundFile from '../sound/Ping1.mp3'; 4 | 5 | export let NOTIFICATION_DEFAULT = 'default'; 6 | export let NOTIFICATION_GRANTED = 'granted'; 7 | export let NOTIFICATION_DENIED = 'denied'; 8 | export let STATUS = ['offline', 'dnd', 'xa', 'away', 'chat', 'online']; 9 | export let SOUNDS = { 10 | MSG: ( incomingMessageSoundFile), 11 | CALL: ( incomingCallSoundFile), 12 | NOTICE: ( noticeSoundFile) 13 | }; 14 | export let REGEX = { 15 | JID: new RegExp('\\b[^"&\'\\/:<>@\\s]+@[\\w-_.]+\\b', 'ig'), 16 | URL: new RegExp(/(aesgcm:\/\/|https?:\/\/|www\.)[^\s<>'"]+/gi), 17 | GEOURI: new RegExp(/geo:(-?\d+(?:\.\d+)?),(-?\d+(?:\.\d+)?)(?:,-?\d+(?:\.\d+)?)?(?:;crs=[\w-]+)?(?:;u=(\d+(?:\.\d+)?))?(?:;[\w-]+=(?:[\w-_.!~*'()]|%[\da-f][\da-f])+)*/, 'g'), 18 | }; 19 | export let NS = { 20 | CARBONS: 'urn:xmpp:carbons:2', 21 | FORWARD: 'urn:xmpp:forward:0' 22 | }; 23 | export let HIDDEN = 'hidden'; 24 | export let SHOWN = 'shown'; 25 | -------------------------------------------------------------------------------- /template/multiUserInvitation.hbs: -------------------------------------------------------------------------------- 1 |

      {{t "muc_invitation"}}

      2 |

      {{t "muc_invitation_explanation"}}

      3 |
      4 |
      5 | 6 |
      7 |

      {{from}}

      8 |
      9 |
      10 |
      11 | 12 |
      13 |

      {{room}}

      14 |
      15 |
      16 |
      17 | 18 |
      19 |

      {{reason}}

      20 |
      21 |
      22 |
      23 |
      24 | 25 | 26 |
      27 |
      28 |
      29 | -------------------------------------------------------------------------------- /images/smiley.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/api/index.ts: -------------------------------------------------------------------------------- 1 | import { testBOSHServer } from './v1/testBOSHServer'; 2 | import Client from '../Client'; 3 | import * as v1 from './v1' 4 | import { AbstractPlugin } from '@src/plugin/AbstractPlugin'; 5 | import { EncryptionPlugin } from '@src/plugin/EncryptionPlugin'; 6 | 7 | export default class JSXC { 8 | public static readonly version = __VERSION__; 9 | 10 | public static readonly testBOSHServer = testBOSHServer; 11 | 12 | public static readonly register = v1.register; 13 | 14 | public static readonly AbstractPlugin = AbstractPlugin; 15 | 16 | public static readonly AbstractEncryptionPlugin = EncryptionPlugin; 17 | 18 | public static readonly jQuery = $; 19 | 20 | private static initialized = false; 21 | 22 | public numberOfCachedAccounts: number; 23 | 24 | public version: string = __VERSION__; 25 | 26 | constructor(options) { 27 | if (JSXC.initialized) { 28 | throw new Error('JSXC was already initialized'); 29 | } 30 | 31 | JSXC.initialized = true; 32 | 33 | this.numberOfCachedAccounts = Client.init(options); 34 | 35 | Object.assign(this, v1); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ### Expected behavior 11 | 12 | ### Actual behavior 13 | 14 | ### Steps to reproduce the behavior 15 | 1. 16 | 2. 17 | 3. 18 | 19 | ### Environment 20 | - **JSXC version:** 21 | - **Host system and version:** 22 | - **Browser vendor and version:** 23 | - **Any browser plugins enabled?** 24 | - **XMPP server vendor and version:** 25 | - **Is your XMPP server working with other clients as expected?** 26 | 27 | ### Logs 28 | #### Javascript 29 | 30 | ``` 31 | ``` 32 | 33 | #### JSXC 34 | 35 | 36 | ``` 37 | ``` 38 | 39 | #### XMPP 40 | 41 | ``` 42 | ``` 43 | 44 | #### Host 45 | 46 | ``` 47 | ``` 48 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Klaus Herberth 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /images/filetypes/folder-shared.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/connection/services/Disco.ts: -------------------------------------------------------------------------------- 1 | import AbstractService from './AbstractService' 2 | import { IJID } from '../../JID.interface' 3 | import * as NS from '../xmpp/namespace' 4 | import { $iq } from '../../vendor/Strophe' 5 | 6 | export default class Disco extends AbstractService { 7 | public getDiscoInfo(jid: IJID, node?: string): Promise { 8 | let attrs = { 9 | xmlns: NS.get('DISCO_INFO'), 10 | node: null 11 | }; 12 | 13 | if (typeof node === 'string' && node.length > 0) { 14 | attrs.node = node; 15 | } 16 | 17 | let iq = $iq({ 18 | to: jid.full, 19 | type: 'get' 20 | }).c('query', attrs); 21 | 22 | return this.sendIQ(iq); 23 | } 24 | 25 | public getDiscoItems(jid: IJID, node?: string): Promise { 26 | let attrs = { 27 | xmlns: NS.get('DISCO_ITEMS'), 28 | node: null 29 | }; 30 | 31 | if (typeof node === 'string' && node.length > 0) { 32 | attrs.node = node; 33 | } 34 | 35 | let iq = $iq({ 36 | to: jid.full, 37 | type: 'get' 38 | }).c('query', attrs); 39 | 40 | return this.sendIQ(iq); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/connection/xmpp/handlers/multiUser/XMessage.ts: -------------------------------------------------------------------------------- 1 | import JID from '../../../../JID' 2 | import AbstractHandler from '../../AbstractHandler' 3 | import { TYPE as NOTICETYPE, FUNCTION as NOTICEFUNCTION } from '../../../../Notice' 4 | 5 | export default class extends AbstractHandler { 6 | public processStanza(stanza: Element) { 7 | let from = new JID($(stanza).attr('from')); 8 | let xElement = $(stanza).find('x[xmlns="http://jabber.org/protocol/muc#user"]'); 9 | 10 | let inviteElement = xElement.find('invite'); 11 | 12 | if (inviteElement.length === 1) { 13 | let host = new JID(inviteElement.attr('from')); 14 | let reason = inviteElement.find('reason').text(); 15 | let password = inviteElement.find('password').text(); 16 | 17 | this.account.getNoticeManager().addNotice({ 18 | title: 'Invitation', 19 | description: `for ${from.bare}`, 20 | type: NOTICETYPE.invitation, 21 | fnName: NOTICEFUNCTION.multiUserInvitation, 22 | fnParams: ['direct', host.bare, from.bare, reason, password, this.account.getUid()] 23 | }); 24 | } 25 | 26 | return this.PRESERVE_HANDLER; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/ui/dialogs/selection.ts: -------------------------------------------------------------------------------- 1 | import Dialog from '../Dialog' 2 | 3 | let selectionTemplate = require('../../../template/selection.hbs'); 4 | 5 | interface ISelectionDialogOptions { 6 | header?: string, 7 | message?: string, 8 | primary?: { 9 | label?: string, 10 | cb: () => any 11 | }, 12 | option?: { 13 | label?: string, 14 | cb: () => any 15 | }, 16 | id?: string 17 | } 18 | 19 | export default function(options: ISelectionDialogOptions) { 20 | let content = selectionTemplate({ 21 | ...options, 22 | hasPrimary: options.primary && typeof options.primary.cb === 'function', 23 | hasOption: options.option && typeof options.option.cb === 'function', 24 | }); 25 | 26 | let dialog = new Dialog(content, true); 27 | let dom = dialog.open(); 28 | 29 | if (options.id) { 30 | dom.attr('data-selection-id', options.id); 31 | } 32 | 33 | dom.find('.jsxc-button--primary').click(function() { 34 | options.primary.cb.call(this, arguments); 35 | 36 | dialog.close(); 37 | }); 38 | dom.find('.jsxc-button--default').click(function() { 39 | options.option.cb.call(this, arguments); 40 | 41 | dialog.close(); 42 | }); 43 | } 44 | -------------------------------------------------------------------------------- /src/util/HookRepository.ts: -------------------------------------------------------------------------------- 1 | 2 | export default class HookRepository { 3 | private hooks = {}; 4 | 5 | public registerHook(eventName: string, func: T) { 6 | if (!this.hooks[eventName]) { 7 | this.hooks[eventName] = []; 8 | } 9 | 10 | this.hooks[eventName].push(func); 11 | } 12 | 13 | public removeHook(eventName: string, func: T) { 14 | let eventNameList = this.hooks[eventName] || []; 15 | 16 | if (eventNameList.indexOf(func) > -1) { 17 | eventNameList = $.grep(eventNameList, function(i) { 18 | return func !== i; 19 | }); 20 | } 21 | 22 | this.hooks[eventName] = eventNameList; 23 | } 24 | 25 | public trigger(targetEventName: string, ...args) { 26 | let hooks = this.hooks; 27 | 28 | let eventNames = Object.keys(hooks); 29 | eventNames.forEach(function(eventName) { 30 | if (targetEventName === eventName || targetEventName.indexOf(eventName + ':') === 0) { 31 | let eventNameHooks = hooks[eventName] || []; 32 | eventNameHooks.forEach(function(hook) { 33 | hook.apply({}, args); 34 | }); 35 | } 36 | }); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /test/Client.ts: -------------------------------------------------------------------------------- 1 | import JID from '../src/JID' 2 | import Account from './AccountStub' 3 | import { IPlugin as PluginInterface } from '../src/plugin/AbstractPlugin' 4 | 5 | export default class Client { 6 | private static account = new Account(); 7 | 8 | public static init() { } 9 | 10 | public static addConnectionPlugin(plugin: PluginInterface) { } 11 | 12 | public static addPreSendMessageHook(hook: (Message, Builder) => void, position?: number) { } 13 | 14 | public static hasFocus() { } 15 | 16 | public static isExtraSmallDevice(): boolean { 17 | return false; 18 | } 19 | 20 | public static isDebugMode(): boolean { 21 | return false; 22 | } 23 | 24 | public static getStorage() { } 25 | 26 | public static getAccout(jid: JID): Account; 27 | public static getAccout(uid?: string): Account; 28 | public static getAccout(): Account { 29 | return Client.account; 30 | } 31 | 32 | public static createAccount(boshUrl: string, jid: string, sid: string, rid: string); 33 | public static createAccount(boshUrl: string, jid: string, password: string); 34 | public static createAccount() { 35 | 36 | } 37 | 38 | public static removeAccount(account: Account) { 39 | 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /scss/partials/_avatar.scss: -------------------------------------------------------------------------------- 1 | .jsxc-avatar { 2 | background-color: $avatar-bg; 3 | background-position: center center; 4 | background-repeat: no-repeat; 5 | background-size: cover; 6 | border-radius: 50%; 7 | color: $avatar-color; 8 | float: left; 9 | font-family: inherit; 10 | font-size: 30px; 11 | font-weight: bold; 12 | height: 36px; 13 | line-height: 36px; 14 | margin: 0 5px; 15 | position: relative; 16 | text-align: center; 17 | width: 36px; 18 | 19 | img { 20 | display: block; 21 | height: 25px; 22 | left: 0; 23 | position: absolute; 24 | top: 0; 25 | width: 25px; 26 | } 27 | 28 | &::before { 29 | border: 2px solid $roster-bg; 30 | left: -6px; 31 | position: absolute; 32 | top: -2px; 33 | } 34 | 35 | &--loading::after { 36 | animation: jsxc-rotate 1s infinite; 37 | border: 10px solid rgba(255, 255, 255, 0.7); 38 | border-left-color: rgba(0, 0, 0, 0.5); 39 | border-radius: 50%; 40 | border-right-color: rgba(0, 0, 0, 0.5); 41 | box-sizing: border-box; 42 | content: ""; 43 | display: block; 44 | height: 20px; 45 | left: 50%; 46 | margin-left: -10px; 47 | margin-top: -10px; 48 | position: absolute; 49 | top: 50%; 50 | width: 20px; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/StateMachine.ts: -------------------------------------------------------------------------------- 1 | import Log from './util/Log' 2 | 3 | export default class StateMachine { 4 | public static STATE = { 5 | INITIATING: 0, 6 | PREVCONFOUND: 1, 7 | SUSPEND: 2, 8 | TRYTOINTERCEPT: 3, 9 | INTERCEPTED: 4, 10 | ESTABLISHING: 5, 11 | READY: 6 12 | }; 13 | 14 | public static UISTATE = { 15 | STANDBY: 0, 16 | INITIATING: 1, 17 | READY: 2, 18 | } 19 | 20 | private static currentState; 21 | private static currentUIState = StateMachine.UISTATE.STANDBY; 22 | 23 | public static changeState(state: number) { 24 | StateMachine.currentState = state; 25 | 26 | Log.debug('State changed to ' + Object.keys(StateMachine.STATE)[state]); 27 | 28 | $(document).trigger('stateChange.jsxc', state); 29 | } 30 | 31 | public static getState(): number { 32 | return StateMachine.currentState; 33 | } 34 | 35 | public static changeUIState(state: number) { 36 | StateMachine.currentUIState = state; 37 | 38 | Log.debug('UI State changed to ' + Object.keys(StateMachine.UISTATE)[state]); 39 | 40 | $(document).trigger('stateUIChange.jsxc', state); 41 | } 42 | 43 | public static getUIState(): number { 44 | return StateMachine.currentUIState; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/bootstrap/plugins.ts: -------------------------------------------------------------------------------- 1 | import Client from '../Client' 2 | import OTRPlugin from '../plugins/otr/Plugin' 3 | import ReceiptPlugin from '../plugins/MessageDeliveryReceiptsPlugin' 4 | import NotificationPlugin from '../plugins/NotificationPlugin' 5 | import MeCommandPlugin from '../plugins/MeCommandPlugin' 6 | import MessageArchiveManagementPlugin from '../plugins/mam/Plugin' 7 | import ChatStatePlugin from '../plugins/chatState/ChatStatePlugin' 8 | import HttpUploadPlugin from '../plugins/httpUpload/HttpUploadPlugin' 9 | import AvatarVCardPlugin from '../plugins/AvatarVCardPlugin' 10 | import CarbonsPlugin from '../plugins/MessageCarbonsPlugin' 11 | import OMEMOPlugin from '../plugins/omemo/Plugin' 12 | import BookmarksPlugin from '@src/plugins/bookmarks/BookmarksPlugin'; 13 | import ChatMarkersPlugin from '@src/plugins/chatMarkers/ChatMarkersPlugin' 14 | 15 | Client.addPlugin(OTRPlugin); 16 | Client.addPlugin(OMEMOPlugin); 17 | Client.addPlugin(ReceiptPlugin); 18 | Client.addPlugin(NotificationPlugin); 19 | Client.addPlugin(MeCommandPlugin); 20 | Client.addPlugin(MessageArchiveManagementPlugin); 21 | Client.addPlugin(ChatStatePlugin); 22 | Client.addPlugin(HttpUploadPlugin); 23 | Client.addPlugin(AvatarVCardPlugin); 24 | Client.addPlugin(CarbonsPlugin); 25 | Client.addPlugin(BookmarksPlugin); 26 | Client.addPlugin(ChatMarkersPlugin); 27 | -------------------------------------------------------------------------------- /src/Migration.ts: -------------------------------------------------------------------------------- 1 | import Storage from './Storage'; 2 | import Log from '@util/Log'; 3 | 4 | const VERSION_KEY = 'version'; 5 | 6 | export default class Migration { 7 | public static run(currentVersion: string, storage: Storage) { 8 | new Migration(currentVersion, storage); 9 | } 10 | 11 | private keys: string[]; 12 | 13 | private constructor(currentVersion: string, private storage: Storage) { 14 | let lastVersion = storage.getItem(VERSION_KEY); 15 | 16 | if (lastVersion !== currentVersion) { 17 | Log.debug('Apply migrations'); 18 | 19 | this.keys = Object.keys(storage.getBackend()); 20 | 21 | this.runV3Migration(); 22 | 23 | storage.setItem(VERSION_KEY, currentVersion); 24 | } 25 | } 26 | 27 | private runV3Migration() { 28 | let backend = this.storage.getBackend(); 29 | 30 | if (!backend.getItem('jsxc:version')) { 31 | return; 32 | } 33 | 34 | Log.debug('Run migration for 3.x'); 35 | 36 | this.keys.forEach(key => { 37 | let matches = key.match(/^jsxc:([^:]+):key$/); 38 | 39 | if (matches) { 40 | let newKey = this.storage.generateKey(matches[1], 'plugin', 'otr', 'key'); 41 | 42 | this.storage.setItem(newKey, backend.getItem(key)); 43 | } 44 | }); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/plugins/omemo/vendor/SessionBuilder.ts: -------------------------------------------------------------------------------- 1 | import { SignalSessionBuilder, ISignalBundleObject, SignalAddress } from './Signal'; 2 | import Address from './Address'; 3 | import Store from '../lib/Store'; 4 | import Bundle from '../lib/Bundle'; 5 | 6 | export class SessionBuilder { 7 | private signalSessionBuilder; 8 | 9 | constructor(address: Address, store: Store) { 10 | let signalAddress = new SignalAddress(address.getName(), address.getDeviceId()); 11 | this.signalSessionBuilder = new SignalSessionBuilder(store.getSignalStore(), signalAddress); 12 | } 13 | 14 | public processPreKey(bundle: Bundle): Promise<[void, boolean]> { 15 | let preKey = bundle.getRandomPreKey(); 16 | let signedPreKey = bundle.getSignedPreKey(); 17 | 18 | let signalBundle: ISignalBundleObject = { 19 | identityKey: bundle.getIdentityKey().getPublic(), 20 | registrationId: 0, 21 | preKey: { 22 | keyId: preKey.getId(), 23 | publicKey: preKey.getPublic(), 24 | }, 25 | signedPreKey: { 26 | keyId: signedPreKey.getId(), 27 | publicKey: signedPreKey.getPublic(), 28 | signature: signedPreKey.getSignature(), 29 | } 30 | }; 31 | 32 | return this.signalSessionBuilder.processPreKey(signalBundle); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /scripts/prepare-commit-msg.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const lintConfig = require('../.commitlintrc.json'); 4 | 5 | const allowedTypes = lintConfig.rules['type-enum'][2]; 6 | const params = (process.env.HUSKY_GIT_PARAMS || '').split(' '); 7 | 8 | const getCommitMessage = () => { 9 | return ` 10 | 11 | # Please enter the commit message for your changes. Lines starting 12 | # with '#' will be ignored, and an empty message aborts the commit. 13 | # In this project we use standardized commit messages. Please adhere 14 | # to the following syntax. 15 | # 16 | # : 17 | # 18 | # [optional body] 19 | # 20 | # [optional footer] 21 | # 22 | # Allowed types are: ${allowedTypes.join(', ')}. 23 | # 24 | # If you fix an issue please add 'fix #NUMBER' to the footer and NOT 25 | # to the description. 26 | # 27 | # See https://www.conventionalcommits.org/ for more details.`; 28 | }; 29 | 30 | // only apply custom messages when run as a standalone `git commit` 31 | // i.e. ignore `--amend` commits, merges, or commits with `-m` 32 | if (params.length > 1) { 33 | process.exit(); 34 | } 35 | 36 | if (!params[0]) { 37 | console.log(getCommitMessage()); 38 | 39 | process.exit(); 40 | } 41 | 42 | const TARGET = path.resolve(process.cwd(), params[0]); 43 | fs.writeFileSync(TARGET, `${getCommitMessage()}`); 44 | -------------------------------------------------------------------------------- /src/util/Pipe.ts: -------------------------------------------------------------------------------- 1 | 2 | const MAX_PRIORITY = 100; 3 | const MIN_PRIORITY = 0; 4 | 5 | type Params = any[]; 6 | 7 | export default class Pipe { 8 | 9 | private pipe = []; 10 | 11 | constructor() { 12 | 13 | } 14 | 15 | public addProcessor(processor: (...args: params) => Promise | params, priority: number = 50) { 16 | if (isNaN(priority) || priority < MIN_PRIORITY || priority > MAX_PRIORITY) { 17 | throw new Error('Priority has to be between 0 and 100'); 18 | } 19 | 20 | if (typeof this.pipe[priority] === 'undefined') { 21 | this.pipe[priority] = []; 22 | } 23 | 24 | this.pipe[priority].push(processor); 25 | } 26 | 27 | public run(...args: params): Promise { 28 | let chain = Promise.resolve(args); 29 | 30 | this.pipe.forEach((processors) => { 31 | if (typeof processors === 'undefined' || processors === null || typeof processors !== 'object' || !processors.length) { 32 | return; 33 | } 34 | 35 | processors.forEach((processor) => { 36 | chain = chain.then((args2: any[]) => { 37 | return processor.apply(this, args2); 38 | }); 39 | }); 40 | }); 41 | 42 | return chain; 43 | } 44 | 45 | public destroy() { 46 | this.pipe = []; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/connection/xmpp/handlers/headlineMessage.ts: -------------------------------------------------------------------------------- 1 | import AbstractHandler from '../AbstractHandler' 2 | import JID from '../../../JID' 3 | import Translation from '../../../util/Translation' 4 | import { TYPE, FUNCTION } from '../../../Notice' 5 | 6 | export default class extends AbstractHandler { 7 | public processStanza(stanza: Element) { 8 | let fromJid = new JID(stanza.getAttribute('from')); 9 | let connection = this.account.getConnection(); 10 | let myJid = connection.getJID(); 11 | 12 | if (!fromJid.isServer) { 13 | if (!this.account.getContact(fromJid)) { 14 | return this.PRESERVE_HANDLER; 15 | } 16 | } else if (fromJid.domain !== myJid.domain) { 17 | return this.PRESERVE_HANDLER; 18 | } 19 | 20 | if ($(stanza).find('body:first').length === 0) { 21 | return this.PRESERVE_HANDLER; 22 | } 23 | 24 | let subject = $(stanza).find('subject:first').text() || Translation.t('Notification'); 25 | let body = $(stanza).find('body:first').text(); 26 | 27 | this.account.getNoticeManager().addNotice({ 28 | title: subject, 29 | description: body, 30 | fnName: FUNCTION.notification, 31 | fnParams: [subject, body, fromJid.full], 32 | type: TYPE.announcement, 33 | }); 34 | 35 | return this.PRESERVE_HANDLER; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /images/microphone_disabled_black.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/microphone_disabled_white.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/api/v1/debug.ts: -------------------------------------------------------------------------------- 1 | import Client from '@src/Client'; 2 | import Log from '@util/Log'; 3 | 4 | export function enableDebugMode() { 5 | let storage = Client.getStorage(); 6 | 7 | storage.setItem('debug', true); 8 | } 9 | 10 | export function disableDebugMode() { 11 | let storage = Client.getStorage(); 12 | 13 | storage.setItem('debug', false); 14 | } 15 | 16 | export function deleteAllData() { 17 | if (!Client.isDebugMode()) { 18 | Log.warn('This action is only available in debug mode.'); 19 | 20 | return 0; 21 | } 22 | 23 | let storage = Client.getStorage(); 24 | let prefix = storage.getPrefix(); 25 | let prefixRegex = new RegExp('^' + prefix); 26 | let backend = storage.getBackend(); 27 | let keys = Object.keys(backend); 28 | let count = 0; 29 | 30 | for (let key of keys) { 31 | if (prefixRegex.test(key) && key !== prefix + 'debug') { 32 | backend.removeItem(key); 33 | count++; 34 | } 35 | } 36 | 37 | return count; 38 | } 39 | 40 | export function deleteObsoleteData() { 41 | let storage = Client.getStorage(); 42 | let backend = storage.getBackend(); 43 | let keys = Object.keys(backend); 44 | let count = 0; 45 | 46 | for (let key of keys) { 47 | if (/^jsxc:/.test(key)) { 48 | backend.removeItem(key); 49 | count++; 50 | } 51 | } 52 | 53 | return count; 54 | } 55 | -------------------------------------------------------------------------------- /src/plugins/omemo/vendor/Signal.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface ISignalBundleObject { 3 | identityKey: ArrayBuffer, 4 | registrationId: number, 5 | preKey: { 6 | keyId: number 7 | publicKey: ArrayBuffer 8 | }, 9 | signedPreKey: { 10 | keyId: number 11 | publicKey: ArrayBuffer 12 | signature: string | ArrayBuffer 13 | } 14 | } 15 | 16 | interface ISignalPreKey { 17 | keyId: number 18 | 19 | keyPair: ISignalKeyPair 20 | } 21 | 22 | interface ISignalSignedPreKey extends ISignalPreKey { 23 | signature: ArrayBuffer 24 | } 25 | 26 | interface ISignalKeyPair { 27 | privKey?: ArrayBuffer 28 | 29 | pubKey: ArrayBuffer 30 | } 31 | 32 | interface ISignalKeyHelper { 33 | generatePreKey: (keyId: number) => Promise 34 | 35 | generateSignedPreKey: (identityKeyPair: ISignalKeyPair, signedKeyId: number) => Promise 36 | 37 | generateIdentityKeyPair: () => Promise 38 | 39 | generateRegistrationId: () => number 40 | } 41 | 42 | let libsignal = ( window).libsignal || {}; 43 | 44 | export let SignalAddress = libsignal.SignalProtocolAddress; 45 | export let SignalKeyHelper: ISignalKeyHelper = libsignal.KeyHelper; 46 | export let SignalSessionBuilder = libsignal.SessionBuilder; 47 | export let SignalSessionCipher = libsignal.SessionCipher; 48 | export let SignalFingerprintGenerator = libsignal.FingerprintGenerator; 49 | -------------------------------------------------------------------------------- /images/pick_up_white_disabled.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/connection/xmpp/handlers/multiUser/DirectInvitation.ts: -------------------------------------------------------------------------------- 1 | import JID from '../../../../JID' 2 | import AbstractHandler from '../../AbstractHandler' 3 | import { TYPE as NOTICETYPE, FUNCTION as NOTICEFUNCTION } from '../../../../Notice' 4 | import Log from '@util/Log'; 5 | 6 | export default class extends AbstractHandler { 7 | public processStanza(stanza: Element): boolean { 8 | let from = new JID($(stanza).attr('from')); 9 | let contact = this.account.getContact(from); 10 | 11 | if (!contact) { 12 | Log.warn('Got invitation from stranger. Ignore silently.'); 13 | } else if (contact.getType() === 'groupchat') { 14 | Log.warn('I don\'t accept direct invitations from MUC rooms.'); 15 | } 16 | 17 | let xElement = $(stanza).find('x[xmlns="jabber:x:conference"]'); 18 | let roomJid = new JID(xElement.attr('jid')); 19 | let password = xElement.attr('password'); 20 | let reason = xElement.attr('reason') || xElement.text(); //pidgin workaround 21 | 22 | this.account.getNoticeManager().addNotice({ 23 | title: 'Invitation', 24 | description: `for ${roomJid.bare}`, 25 | type: NOTICETYPE.invitation, 26 | fnName: NOTICEFUNCTION.multiUserInvitation, 27 | fnParams: ['direct', from.bare, roomJid.bare, reason, password, this.account.getUid()] 28 | }); 29 | 30 | return this.PRESERVE_HANDLER; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /images/mute_black.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/filetypes/folder-public.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/connection/xmpp/handlers/jingle.ts: -------------------------------------------------------------------------------- 1 | import Account from '../../../Account' 2 | import AbstractHandler from '../AbstractHandler' 3 | import { STANZA_JINGLE_KEY } from '../../AbstractConnection' 4 | 5 | const FEATURES = [ 6 | 'urn:xmpp:jingle:1', 7 | 'urn:xmpp:jingle:apps:rtp:1', 8 | 'urn:xmpp:jingle:apps:rtp:audio', 9 | 'urn:xmpp:jingle:apps:rtp:video', 10 | 'urn:xmpp:jingle:apps:rtp:rtcb-fb:0', 11 | 'urn:xmpp:jingle:apps:rtp:rtp-hdrext:0', 12 | 'urn:xmpp:jingle:apps:rtp:ssma:0', 13 | 'urn:xmpp:jingle:apps:dtls:0', 14 | 'urn:xmpp:jingle:apps:grouping:0', 15 | 'urn:xmpp:jingle:apps:file-transfer:3', 16 | 'urn:xmpp:jingle:transports:ice-udp:1', 17 | 'urn:xmpp:jingle:transports.dtls-sctp:1', 18 | 'urn:ietf:rfc:3264', 19 | 'urn:ietf:rfc:5576', 20 | 'urn:ietf:rfc:5888' 21 | ]; 22 | 23 | export default class extends AbstractHandler { 24 | constructor(account: Account) { 25 | super(account); 26 | 27 | for (let feature of FEATURES) { 28 | account.getDiscoInfo().addFeature(feature); 29 | } 30 | } 31 | 32 | public processStanza(stanza: Element) { 33 | let connection = this.account.getConnection(); 34 | let storage = this.account.getSessionStorage(); 35 | 36 | storage.setItem(STANZA_JINGLE_KEY, stanza.outerHTML); 37 | storage.removeItem(STANZA_JINGLE_KEY); 38 | 39 | connection.getJingleHandler().onJingle(stanza); 40 | 41 | return this.PRESERVE_HANDLER; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /template/roster.hbs: -------------------------------------------------------------------------------- 1 |
      2 |
      3 |
        4 |
        5 |
        6 |
        7 |
        8 | {{#> menu 9 | classes="jsxc-menu--pushup jsxc-grow jsxc-js-presence-menu" 10 | button-label=(t "Offline")}} 11 |
      • {{t "Online"}}
      • 12 |
      • {{t "Chatty"}}
      • 13 |
      • {{t "Away"}}
      • 14 |
      • {{t "Extended_away"}}
      • 15 |
      • {{t "dnd"}}
      • 16 |
      • {{t "Offline"}}
      • 17 | {{/menu}} 18 | 19 | {{> menu classes="jsxc-menu--pushup jsxc-js-notice-menu" button-classes="jsxc-bounce"}} 20 | 21 | {{> menu classes="jsxc-menu--pushup jsxc-js-main-menu" button-classes="jsxc-icon jsxc-icon--menu-dark jsxc-icon--clickable"}} 22 |
        23 |
        24 |
        25 | -------------------------------------------------------------------------------- /example/css/example.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding-top: 30px; 3 | } 4 | 5 | .logout { 6 | display: none; 7 | } 8 | 9 | .jsxc-org, .localhost { 10 | display: none; 11 | } 12 | 13 | #content form .alert { 14 | display: none; 15 | } 16 | 17 | #content h3 { 18 | border-bottom: 1px solid #e5e5e5; 19 | margin-bottom: 20px; 20 | } 21 | 22 | #content .col-md-4 .form { 23 | min-height: 180px; 24 | } 25 | 26 | #content .row:first-child { 27 | margin-bottom: 30px; 28 | } 29 | 30 | #content .col-md-4>p { 31 | margin-bottom: 30px; 32 | } 33 | 34 | #server-flash { 35 | margin: 0; 36 | margin-top: 10px; 37 | } 38 | 39 | #server-flash:before { 40 | content: "\e031"; 41 | position: relative; 42 | top: 1px; 43 | padding-right: 5px; 44 | display: inline-block; 45 | font-family: 'Glyphicons Halflings'; 46 | font-style: normal; 47 | font-weight: 400; 48 | line-height: 1; 49 | -webkit-font-smoothing: antialiased; 50 | } 51 | 52 | #server-flash.success:before { 53 | content: "\e089"; 54 | color: green; 55 | } 56 | 57 | #server-flash.fail:before { 58 | content: "\e088"; 59 | color: red; 60 | } 61 | 62 | details { 63 | padding: 1em; 64 | margin-bottom: 1em; 65 | } 66 | 67 | summary { 68 | margin: -1em -1em 1em -1em; 69 | cursor: pointer; 70 | } 71 | 72 | body { 73 | border-top: 5px solid transparent; 74 | } 75 | 76 | body.jsxc-master { 77 | border-top: 5px solid green; 78 | } 79 | 80 | body.jsxc-slave { 81 | border-top: 5px solid orange; 82 | } 83 | -------------------------------------------------------------------------------- /src/plugins/bookmarks/services/LocalService.ts: -------------------------------------------------------------------------------- 1 | import AbstractService from './AbstractService'; 2 | import RoomBookmark from '../RoomBookmark'; 3 | import IStorage from '@src/Storage.interface'; 4 | import JID from '@src/JID'; 5 | import { IJID } from '@src/JID.interface'; 6 | 7 | export default class LocalService extends AbstractService { 8 | constructor(private storage: IStorage) { 9 | super(); 10 | } 11 | 12 | public getName(): string { 13 | return 'local'; 14 | } 15 | 16 | public async getRooms(): Promise { 17 | let data = this.storage.getItem('rooms') || {}; 18 | let rooms = []; 19 | 20 | for (let id in data) { 21 | let roomData = data[id]; 22 | 23 | rooms.push(new RoomBookmark(new JID(id), roomData.alias, roomData.nickname, roomData.autoJoin, roomData.password)); 24 | } 25 | 26 | return rooms; 27 | } 28 | 29 | public async addRoom(room: RoomBookmark) { 30 | let data = this.storage.getItem('rooms') || {}; 31 | let id = room.getJid().bare; 32 | 33 | data[id] = { 34 | alias: room.getAlias(), 35 | nickname: room.getNickname(), 36 | autoJoin: room.isAutoJoin(), 37 | password: room.getPassword(), 38 | }; 39 | 40 | this.storage.setItem('rooms', data); 41 | } 42 | 43 | public async removeRoom(id: IJID) { 44 | let data = this.storage.getItem('rooms') || {}; 45 | 46 | delete data[id.bare]; 47 | 48 | this.storage.setItem('rooms', data); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/ui/DialogListItem.ts: -------------------------------------------------------------------------------- 1 | 2 | export default class ListItem { 3 | private element: JQuery; 4 | 5 | constructor(private primaryText: string, private secondaryText?: string, private onClickHandler?, private avatar?: JQuery, private secondaryAction?: JQuery) { 6 | 7 | } 8 | 9 | public getDOM(): JQuery { 10 | if (!this.element) { 11 | this.generateDOM(); 12 | } 13 | 14 | return this.element; 15 | } 16 | 17 | private generateDOM() { 18 | this.element = $('
      • '); 19 | this.element.addClass('jsxc-list__item'); 20 | 21 | if (typeof this.onClickHandler === 'function') { 22 | this.element.addClass('jsxc-list__item--clickable'); 23 | this.element.on('click', () => this.onClickHandler()); 24 | } 25 | 26 | if (this.avatar) { 27 | this.avatar.addClass('jsxc-list__avatar'); 28 | this.element.append(this.avatar); 29 | } 30 | 31 | let textElement = $('
        '); 32 | textElement.addClass('jsxc-list__text'); 33 | textElement.appendTo(this.element); 34 | 35 | $('
        ').text(this.primaryText).addClass('jsxc-list__text__primary').appendTo(textElement); 36 | 37 | if (this.secondaryText) { 38 | $('
        ').text(this.secondaryText).addClass('jsxc-list__text__secondary').appendTo(textElement); 39 | } 40 | 41 | if (this.secondaryAction) { 42 | this.secondaryAction.addClass('jsxc-list__secondary-action'); 43 | this.element.append(this.secondaryAction); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /template/loginBox.hbs: -------------------------------------------------------------------------------- 1 |

        {{t "Login"}}

        2 | 3 |
        4 | {{#if showBoshUrlField}} 5 |
        6 | 7 |
        8 | 9 |
        10 |
        11 | {{/if}} 12 |
        13 | 14 |
        15 | 16 |
        17 |
        18 |
        19 | 20 |
        21 | 22 |
        23 |
        24 |
        {{t "Sorry_we_cant_authentikate_"}}
        25 |
        26 |
        27 | 28 | 29 |
        30 |
        31 |
        32 | -------------------------------------------------------------------------------- /images/padlock_open_disabled_black.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /template/contact.hbs: -------------------------------------------------------------------------------- 1 |

        {{t "Add_buddy"}}

        2 | 3 |

        {{t "Type_in_the_full_username_"}}

        4 | 5 |
        6 |
        7 | 8 |
        9 | 12 |
        13 |
        14 | 15 |
        16 | 17 |
        18 | 19 |
        20 |
        21 | 22 | 23 | 24 |
        25 | 26 |
        27 | 28 |
        29 |
        30 | 31 |
        32 |
        33 | 34 | 35 |
        36 |
        37 |
        38 | -------------------------------------------------------------------------------- /src/plugins/bookmarks/BookmarksPlugin.ts: -------------------------------------------------------------------------------- 1 | import { AbstractPlugin, IMetaData } from '../../plugin/AbstractPlugin' 2 | import PluginAPI from '../../plugin/PluginAPI' 3 | import { PubSubService } from './services/PubSubService'; 4 | import LocalService from './services/LocalService'; 5 | import BookmarkProvider from './BookmarkProvider'; 6 | import Translation from '@util/Translation'; 7 | 8 | const MIN_VERSION = '4.0.0'; 9 | const MAX_VERSION = '99.0.0'; 10 | 11 | export default class BookmarksPlugin extends AbstractPlugin { 12 | public static getId(): string { 13 | return 'bookmarks'; 14 | } 15 | 16 | public static getName(): string { 17 | return 'Bookmarks'; 18 | } 19 | 20 | public static getMetaData(): IMetaData { 21 | return { 22 | description: Translation.t('setting-bookmarks-enable'), 23 | xeps: [{ 24 | id: 'XEP-0048', 25 | name: 'Bookmarks', 26 | version: '1.1', 27 | }] 28 | } 29 | } 30 | 31 | constructor(pluginAPI: PluginAPI) { 32 | super(MIN_VERSION, MAX_VERSION, pluginAPI); 33 | 34 | let contactManager = pluginAPI.getContactManager(); 35 | let provider = new BookmarkProvider(contactManager, pluginAPI.createMultiUserContact.bind(pluginAPI)); 36 | 37 | provider.registerService(new LocalService(pluginAPI.getStorage())); 38 | 39 | //@TODO test if pubsub is available 40 | let pubSub = new PubSubService(pluginAPI.getConnection()); 41 | provider.registerService(pubSub); 42 | 43 | pluginAPI.registerContactProvider(provider); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/FallbackContactProvider.ts: -------------------------------------------------------------------------------- 1 | import ContactProvider from './ContactProvider'; 2 | import { IContact } from './Contact.interface'; 3 | import { IJID } from './JID.interface'; 4 | import Contact from './Contact'; 5 | import Account from './Account'; 6 | import MultiUserContact from './MultiUserContact'; 7 | import ContactManager from './ContactManager'; 8 | 9 | export const FALLBACK_ID = 'fallback'; 10 | 11 | export default class FallbackContactProvider extends ContactProvider { 12 | constructor(protected contactManager: ContactManager, private account: Account) { 13 | super(contactManager); 14 | } 15 | 16 | public getUid(): string { 17 | return FALLBACK_ID; 18 | } 19 | 20 | public load(): Promise { 21 | return Promise.resolve([]); 22 | } 23 | 24 | public add(contact: IContact): Promise { 25 | return Promise.resolve(false); 26 | } 27 | 28 | public createContact(jid: IJID, name?: string): IContact 29 | public createContact(id: string): IContact 30 | public createContact() { 31 | if (typeof arguments[0] === 'string') { 32 | let contact = new Contact(this.account, arguments[0]); 33 | 34 | if (contact.isGroupChat()) { 35 | return new MultiUserContact(this.account, arguments[0]); 36 | } else { 37 | return contact; 38 | } 39 | } 40 | 41 | let contact = new Contact(this.account, arguments[0], arguments[1]); 42 | contact.setProvider(this); 43 | 44 | return contact; 45 | } 46 | 47 | public deleteContact(jid: IJID): Promise { 48 | return Promise.resolve(); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /scss/partials/_button.scss: -------------------------------------------------------------------------------- 1 | .jsxc-button { 2 | background-image: none; 3 | // border: 1px solid transparent; 4 | // border-radius: 4px; 5 | border: 0; 6 | cursor: pointer; 7 | display: inline-block; 8 | font-size: 14px; 9 | font-weight: 400; 10 | line-height: 1.42857143; 11 | margin: 0 2px; 12 | min-width: 25px; 13 | padding: 6px 12px; 14 | padding: 10px 16px; 15 | text-align: center; 16 | text-decoration: none; 17 | transition: background-color 0.5s; 18 | user-select: none; 19 | vertical-align: middle; 20 | white-space: nowrap; 21 | width: auto; 22 | 23 | &--block { 24 | display: block; 25 | width: 100%; 26 | } 27 | 28 | &--default { 29 | background-color: rgba(240, 240, 240, 0.9); 30 | border-color: #ccc; 31 | color: #555; 32 | 33 | &:hover { 34 | background-color: #d6d6d6; 35 | } 36 | } 37 | 38 | &--primary { 39 | background-color: #337ab7; 40 | border-color: #2e6da4; 41 | color: #fff; 42 | 43 | &:hover { 44 | background-color: #296496; 45 | } 46 | } 47 | 48 | &--secondary { 49 | background-color: #7daad2; 50 | border-color: #658caf; 51 | color: #fff; 52 | 53 | &:hover { 54 | background-color: #6b99c1; 55 | } 56 | } 57 | 58 | &--success { 59 | background-color: #6bad50; 60 | border-color: #487735; 61 | color: #fff; 62 | 63 | &:hover { 64 | background-color: #5a9443; 65 | } 66 | } 67 | 68 | &[disabled], 69 | &[disabled]:hover { 70 | background-color: #337ab7; 71 | border-color: #2e6da4; 72 | color: #fff; 73 | cursor: not-allowed; 74 | opacity: 0.65; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/ui/ChatWindowFileTransferHandler.ts: -------------------------------------------------------------------------------- 1 | import ChatWindow from '../ui/ChatWindow' 2 | import Attachment from '../Attachment' 3 | 4 | export default class FileTransferHandler { 5 | private handlerElement; 6 | 7 | constructor(private chatWindow: ChatWindow) { 8 | this.handlerElement = this.chatWindow.getDom().find('.jsxc-file-transfer'); 9 | 10 | this.handlerElement.on('click', this.showFileSelection); 11 | 12 | this.chatWindow.getDom().find('.jsxc-window').on('drop', (ev) => { 13 | ev.preventDefault(); 14 | 15 | let files = ( ev.originalEvent).dataTransfer.files; 16 | 17 | if (files && files.length) { 18 | this.fileSelected(files[0]); 19 | } 20 | }); 21 | } 22 | 23 | private showFileSelection = (ev) => { 24 | if (ev.target !== this.handlerElement.get(0)) { 25 | // prevent bubbled event 26 | return; 27 | } 28 | 29 | this.showFileSelectionDialog(); 30 | } 31 | 32 | private showFileSelectionDialog() { 33 | let labelElement = this.handlerElement.find('label'); 34 | let fileElement = this.handlerElement.find('input'); 35 | 36 | // open file selection for user 37 | labelElement.click(); 38 | 39 | fileElement.off('change').change((ev) => { 40 | let file: File = ev.target.files[0]; // FileList object 41 | 42 | if (!file) { 43 | return; 44 | } 45 | 46 | this.fileSelected(file); 47 | }); 48 | } 49 | 50 | private fileSelected(file: File) { 51 | let attachment = new Attachment(file); 52 | this.chatWindow.setAttachment(attachment); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /images/presence/online.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 37 | 39 | 41 | 42 | 44 | image/svg+xml 45 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /template/bookmark.hbs: -------------------------------------------------------------------------------- 1 |

        2 |
        3 |
        4 | 5 |
        6 | 7 |
        8 |
        9 |
        10 | 11 |
        12 | 13 |
        14 |
        15 |
        16 |
        17 |
        18 | 21 |
        22 |
        23 |
        24 |
        25 |
        26 |
        27 | 30 |
        31 |
        32 |
        33 |
        34 |
        35 | 36 | 37 |
        38 |
        39 |
        40 | -------------------------------------------------------------------------------- /src/connection/services/PEP.ts: -------------------------------------------------------------------------------- 1 | import AbstractService from './AbstractService' 2 | import { $iq } from '../../vendor/Strophe' 3 | 4 | export default class PEP extends AbstractService { 5 | 6 | public subscribe(node: string, handler: (stanza: string) => boolean, force: boolean = false) { 7 | this.account.getDiscoInfo().addFeature(node); 8 | this.account.getDiscoInfo().addFeature(`${node}+notify`); 9 | 10 | this.connection.registerHandler(handler, 'http://jabber.org/protocol/pubsub#event', 'message', null, null, null); 11 | 12 | if (force) { 13 | return this.connection.sendPresence(); 14 | } 15 | } 16 | 17 | public unsubscribe(node: string, force: boolean = false) { 18 | this.account.getDiscoInfo().removeFeature(node) 19 | this.account.getDiscoInfo().removeFeature(`${node}+notify`) 20 | 21 | if (force) { 22 | return this.connection.sendPresence(); 23 | } 24 | } 25 | 26 | public publish(node: string, element: Element, id?: string): Promise { 27 | let iqStanza = $iq({ 28 | type: 'set', 29 | }).c('pubsub', { 30 | xmlns: 'http://jabber.org/protocol/pubsub' 31 | }).c('publish', { 32 | node 33 | }).c('item', { 34 | id 35 | }).cnode(element); 36 | 37 | return this.sendIQ(iqStanza); 38 | } 39 | 40 | public retrieveItems(node: string, jid?: string) { 41 | let iq = $iq({ 42 | to: jid, 43 | type: 'get' 44 | }); 45 | 46 | iq.c('pubsub', { 47 | xmlns: 'http://jabber.org/protocol/pubsub' 48 | }); 49 | iq.c('items', { 50 | node 51 | }); 52 | 53 | return this.sendIQ(iq); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /images/filetypes/application.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /scss/modules/_animation.scss: -------------------------------------------------------------------------------- 1 | @keyframes jsxc-bounce-6 { 2 | 0% { 3 | transform: scale(1, 1) translateY(0); 4 | } 5 | 6 | 10% { 7 | transform: scale(1.1, 0.9) translateY(0); 8 | } 9 | 10 | 30% { 11 | transform: scale(0.9, 1.1) translateY(-10px); 12 | } 13 | 14 | 50% { 15 | transform: scale(1.05, 0.95) translateY(0); 16 | } 17 | 18 | 57% { 19 | transform: scale(1, 1) translateY(-1px); 20 | } 21 | 22 | 64% { 23 | transform: scale(1, 1) translateY(0); 24 | } 25 | 26 | 100% { 27 | transform: scale(1, 1) translateY(0); 28 | } 29 | } 30 | 31 | @keyframes jsxc-establishing { 32 | 0% { 33 | background-color: $establishing-color1; 34 | border-width: 0; 35 | margin-left: -20px; 36 | width: 40px; 37 | } 38 | 39 | 50% { 40 | background-color: $establishing-color2; 41 | margin-left: -40px; 42 | width: 80px; 43 | } 44 | 45 | 100% { 46 | background-color: $establishing-color1; 47 | border-width: 0; 48 | margin-left: -20px; 49 | width: 40px; 50 | } 51 | } 52 | 53 | @keyframes jsxc-ringing { 54 | 0% { 55 | background-color: $ringing-color1; 56 | height: 20px; 57 | margin-left: -10px; 58 | margin-top: -10px; 59 | width: 20px; 60 | } 61 | 62 | 50% { 63 | background-color: $ringing-color2; 64 | height: 80px; 65 | margin-left: -40px; 66 | margin-top: -40px; 67 | width: 80px; 68 | } 69 | 70 | 100% { 71 | background-color: $ringing-color1; 72 | height: 20px; 73 | margin-left: -10px; 74 | margin-top: -10px; 75 | width: 20px; 76 | } 77 | } 78 | 79 | @keyframes jsxc-rotate { 80 | 0% { 81 | transform: rotate(0deg); 82 | } 83 | 84 | 100% { 85 | transform: rotate(360deg); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/DiscoInfoChangeable.ts: -------------------------------------------------------------------------------- 1 | import DiscoInfo from './DiscoInfo' 2 | import DiscoInfoVersion from './DiscoInfoVersion'; 3 | 4 | export default class DiscoInfoChangeable extends DiscoInfo { 5 | 6 | constructor(id: string) { 7 | super(id); 8 | } 9 | 10 | public getCapsVersion(): String { 11 | return DiscoInfoVersion.generate(this.getIdentities(), this.getFeatures(), []); 12 | } 13 | 14 | public addIdentity(category: string, type: string, name: string = '', lang: string = ''): boolean { 15 | let identities = this.getIdentities(); 16 | 17 | for (let identity of identities) { 18 | if (identity.category === category && 19 | identity.type === type && 20 | identity.name === name && 21 | identity.lang === lang) { 22 | return false; 23 | } 24 | } 25 | 26 | identities.push({ 27 | category, 28 | type, 29 | name, 30 | lang 31 | }); 32 | this.data.set('identities', identities); 33 | 34 | return true; 35 | } 36 | 37 | public addFeature(feature: string): boolean { 38 | let features = this.getFeatures(); 39 | 40 | if (features.indexOf(feature) > -1) { 41 | return false; 42 | } 43 | 44 | features.push(feature); 45 | this.data.set('features', features); 46 | 47 | return true; 48 | } 49 | 50 | public removeFeature(feature: string): boolean { 51 | let features = this.getFeatures(); 52 | let index = features.indexOf(feature); 53 | 54 | if (index > -1) { 55 | features.splice(index, 1); 56 | this.data.set('features', features); 57 | 58 | return true; 59 | } 60 | 61 | return false; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /images/XMPP_logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example/js/example.js: -------------------------------------------------------------------------------- 1 | let jsxc = new JSXC({ 2 | loadConnectionOptions: (username, password) => { 3 | return Promise.resolve({ 4 | xmpp: { 5 | url: $('#bosh-url').val(), 6 | domain: $('#xmpp-domain').val(), 7 | } 8 | }); 9 | }, 10 | connectionCallback: (jid, status) => { 11 | const CONNECTED = 5; 12 | const ATTACHED = 8; 13 | 14 | if (status === CONNECTED || status === ATTACHED) { 15 | $('.logout').show(); 16 | $('.submit').hide(); 17 | } else { 18 | $('.logout').hide(); 19 | $('.submit').show(); 20 | } 21 | } 22 | }); 23 | 24 | subscribeToInstantLogin(); 25 | watchForm(); 26 | watchLogoutButton(); 27 | 28 | function watchForm() { 29 | let formElement = $('#watch-form'); 30 | let usernameElement = $('#watch-username'); 31 | let passwordElement = $('#watch-password'); 32 | 33 | jsxc.watchForm(formElement, usernameElement, passwordElement); 34 | } 35 | 36 | function watchLogoutButton() { 37 | let buttonElements = $('.logout'); 38 | 39 | jsxc.watchLogoutClick(buttonElements); 40 | } 41 | 42 | function subscribeToInstantLogin() { 43 | $('#instant-login-form').submit(function(ev) { 44 | var url = $('#bosh-url').val(); 45 | var domain = $('#xmpp-domain').val(); 46 | 47 | var username = $(this).find('[name="username"]').val(); 48 | var password = $(this).find('[name="password"]').val(); 49 | 50 | var jid = username + '@' + domain; 51 | 52 | jsxc.start(url, jid, password) 53 | .then(function() { 54 | console.log('>>> CONNECTION READY') 55 | }).catch(function(err) { 56 | console.log('>>> catch', err) 57 | }) 58 | 59 | return false; 60 | }); 61 | } 62 | -------------------------------------------------------------------------------- /src/util/Translation.ts: -------------------------------------------------------------------------------- 1 | import i18next from 'i18next' 2 | import Log from '@util/Log'; 3 | import Client from '@src/Client'; 4 | import LanguageDetector from 'i18next-browser-languagedetector' 5 | 6 | let resources = __LANGS__.reduce((resources, lang) => { 7 | resources[lang] = require(`../../locales/${lang}.json`); 8 | 9 | return resources; 10 | }, {}); 11 | 12 | export default class Translation { 13 | private static initialized = false; 14 | 15 | public static initialize() { 16 | if (Client.getOption('autoLang')) { 17 | i18next.use(LanguageDetector); 18 | } 19 | 20 | i18next.init({ 21 | debug: Client.isDebugMode(), 22 | lng: Client.getOption('lang'), 23 | fallbackLng: 'en', 24 | returnNull: false, 25 | resources, 26 | interpolation: { 27 | prefix: '__', 28 | suffix: '__' 29 | }, 30 | saveMissing: true, 31 | detection: { 32 | order: ['querystring', 'navigator', 'htmlTag', 'path', 'subdomain'], 33 | }, 34 | }); 35 | 36 | i18next.on('missingKey', function(language, namespace, key, res) { 37 | Log.info(`[i18n] Translation of "${key}" is missing for language "${language}". Namespace: ${namespace}. Resource: ${res}.`); 38 | }); 39 | 40 | Translation.initialized = true; 41 | } 42 | 43 | public static t(text: string, param?): string { 44 | if (!Translation.initialized) { 45 | Log.warn('Translator not initialized'); 46 | 47 | return text; 48 | } 49 | 50 | let translatedString = i18next.t(text, param); 51 | 52 | return translatedString; 53 | } 54 | 55 | public static getCurrentLanguage() { 56 | return i18next.language; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Avatar.ts: -------------------------------------------------------------------------------- 1 | import { IAvatar } from './Avatar.interface' 2 | import Client from './Client' 3 | import PersistentMap from './util/PersistentMap' 4 | import * as sha1 from 'js-sha1' 5 | 6 | export default class implements IAvatar { 7 | private properties: PersistentMap; 8 | 9 | constructor(private sha1Hash: string, type?: string, data?: string) { 10 | let storage = Client.getStorage(); 11 | this.properties = new PersistentMap(storage, sha1Hash); 12 | 13 | if (!this.properties.get('data')) { 14 | if (data && type) { 15 | let expectedHash = this.calculateHash(data); 16 | 17 | if (expectedHash !== sha1Hash) { 18 | throw new Error('SHA-1 hash doesnt match'); 19 | } 20 | 21 | this.properties.set('data', data); 22 | this.properties.set('type', type); 23 | } else { 24 | throw new Error('Avatar not found'); 25 | } 26 | } 27 | } 28 | 29 | public getData(): string { 30 | return this.properties.get('data'); 31 | } 32 | 33 | public getType(): string { 34 | return this.properties.get('type'); 35 | } 36 | 37 | public getHash(): string { 38 | return this.sha1Hash; 39 | } 40 | 41 | private calculateHash(data: string): string { 42 | let base64 = data.replace(/^.+;base64,/, ''); 43 | let buffer = this.base64ToArrayBuffer(base64); 44 | 45 | return sha1(buffer); 46 | } 47 | 48 | private base64ToArrayBuffer(base64String) { 49 | let binaryString = window.atob(base64String); 50 | let bytes = new Uint8Array(binaryString.length); 51 | 52 | for (let i = 0; i < binaryString.length; i++) { 53 | bytes[i] = binaryString.charCodeAt(i); 54 | } 55 | 56 | return bytes.buffer; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/JingleStreamSession.ts: -------------------------------------------------------------------------------- 1 | import JingleHandler from '@connection/JingleHandler'; 2 | import Log from '@util/Log'; 3 | import JingleMediaSession from './JingleMediaSession'; 4 | import Notification from './Notification'; 5 | import Translation from '@util/Translation'; 6 | import { SOUNDS } from './CONST'; 7 | 8 | export default class JingleStreamSession extends JingleMediaSession { 9 | 10 | public onOnceIncoming() { 11 | Notification.notify({ 12 | title: Translation.t('Incoming_stream'), 13 | message: Translation.t('from_sender') + this.peerContact.getName(), 14 | source: this.peerContact, 15 | }); 16 | 17 | Notification.playSound(SOUNDS.CALL, true, true); 18 | 19 | this.on('terminated', () => { 20 | Notification.stopSound(); 21 | }); 22 | 23 | this.on('aborted', () => { 24 | Notification.stopSound(); 25 | }); 26 | 27 | this.on('adopt', () => { 28 | Notification.stopSound(); 29 | }); 30 | 31 | // send signal to partner 32 | this.session.ring(); 33 | } 34 | 35 | protected onIncoming() { 36 | Log.debug('incoming stream from ' + this.session.peerID); 37 | 38 | let videoDialog = JingleHandler.getVideoDialog(); 39 | 40 | videoDialog.showCallDialog(this).then(() => { 41 | videoDialog.addSession(this); 42 | videoDialog.showVideoWindow(); 43 | 44 | this.session.accept(); 45 | }).catch((reason) => { 46 | 47 | //@TODO hide user media request overlay 48 | 49 | //@TODO post reason to chat window 50 | if (reason !== 'aborted') { 51 | Log.warn('Decline call', reason) 52 | 53 | this.session.decline(); 54 | } 55 | }); 56 | } 57 | 58 | public getMediaRequest() { 59 | return []; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/plugins/MeCommandPlugin.ts: -------------------------------------------------------------------------------- 1 | import { AbstractPlugin, IMetaData } from '../plugin/AbstractPlugin' 2 | import PluginAPI from '../plugin/PluginAPI' 3 | import Contact from '../Contact' 4 | import Translation from '@util/Translation'; 5 | import { DIRECTION } from '@src/Message.interface'; 6 | 7 | const MIN_VERSION = '4.0.0'; 8 | const MAX_VERSION = '99.0.0'; 9 | 10 | export default class MeCommandPlugin extends AbstractPlugin { 11 | public static getId(): string { 12 | return 'me-command'; 13 | } 14 | 15 | public static getName(): string { 16 | return 'The /me Command'; 17 | } 18 | 19 | public static getMetaData(): IMetaData { 20 | return { 21 | description: Translation.t('setting-meCommand-enable'), 22 | xeps: [{ 23 | id: 'XEP-0245', 24 | name: 'The /me Command', 25 | version: '1.0', 26 | }] 27 | } 28 | } 29 | 30 | constructor(pluginAPI: PluginAPI) { 31 | super(MIN_VERSION, MAX_VERSION, pluginAPI); 32 | 33 | pluginAPI.registerTextFormatter(this.textFormatter); 34 | } 35 | 36 | private textFormatter = (plaintext: string, direction: DIRECTION, contact: Contact, senderName: string) => { 37 | let meRegex = /^\/me /; 38 | 39 | if (direction !== DIRECTION.IN) { 40 | return plaintext.replace(meRegex, `/me `); 41 | } 42 | 43 | if (!senderName && !contact) { 44 | return plaintext; 45 | } 46 | 47 | if (meRegex.test(plaintext)) { 48 | let name = senderName || contact.getName(); 49 | 50 | if (name.indexOf('@') > -1) { 51 | name = name.slice(0, name.indexOf('@')); 52 | } 53 | 54 | plaintext = plaintext.replace(meRegex, `${name} `); 55 | } 56 | 57 | return plaintext; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /scripts/search-blacklist.js: -------------------------------------------------------------------------------- 1 | require('colors'); 2 | const childProcess = require('child_process'); 3 | 4 | function escapeRegex(str) { 5 | return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); 6 | } 7 | 8 | const BLACKLIST = ['console.']; 9 | const BLACKLIST_REGEX = new RegExp(BLACKLIST.map(word => escapeRegex(word)).join('|'), 'g'); 10 | 11 | function exec(command) { 12 | return new Promise((resolve, reject) => { 13 | childProcess.exec(command, (error, stdout, stderr) => { 14 | if (error) { 15 | reject(error); 16 | } else { 17 | resolve([stdout, stderr]); 18 | } 19 | }); 20 | }); 21 | } 22 | 23 | async function getDiff() { 24 | let [output] = await exec('git diff --staged'); 25 | 26 | return output.split('\n'); 27 | } 28 | 29 | async function getAdditions() { 30 | let diff = await getDiff(); 31 | 32 | return diff.filter(line => /^\+/.test(line)); 33 | } 34 | 35 | getAdditions().then(additions => { 36 | let output = []; 37 | let currentFile; 38 | let currentFileAdded = false; 39 | 40 | for (let addition of additions) { 41 | if (/^\+\+\+/.test(addition)) { 42 | currentFile = addition; 43 | currentFileAdded = false; 44 | } else if (BLACKLIST_REGEX.test(addition)) { 45 | if (!currentFileAdded) { 46 | output.push(currentFile); 47 | currentFileAdded = true; 48 | } 49 | 50 | output.push(addition); 51 | } 52 | } 53 | 54 | if (output.length) { 55 | console.log(`✖ Found some blacklisted terms. Please remove them before you commit our changes.`.red); 56 | console.log(output.join('\n').replace(BLACKLIST_REGEX, (match) => match.yellow)); 57 | 58 | process.exit(1); 59 | } 60 | }) 61 | -------------------------------------------------------------------------------- /src/plugins/omemo/vendor/KeyHelper.ts: -------------------------------------------------------------------------------- 1 | import PreKey from '../model/PreKey'; 2 | import SignedPreKey from '../model/SignedPreKey'; 3 | import IdentityKey from '../model/IdentityKey'; 4 | import { SignalKeyHelper } from './Signal'; 5 | 6 | export class KeyHelper { 7 | public static async generatePreKey(keyId: number): Promise { 8 | let signalPreKey = await SignalKeyHelper.generatePreKey(keyId); 9 | 10 | return new PreKey({ 11 | keyId: signalPreKey.keyId, 12 | keyPair: { 13 | publicKey: signalPreKey.keyPair.pubKey, 14 | privateKey: signalPreKey.keyPair.privKey, 15 | }, 16 | }); 17 | } 18 | 19 | public static async generateSignedPreKey(identityKey: IdentityKey, signedKeyId: number): Promise { 20 | let signalIdentityKey = { 21 | pubKey: identityKey.getPublic(), 22 | privKey: identityKey.getPrivate(), 23 | }; 24 | 25 | let signalSignedPreKey = await SignalKeyHelper.generateSignedPreKey(signalIdentityKey, signedKeyId); 26 | 27 | return new SignedPreKey({ 28 | keyId: signalSignedPreKey.keyId, 29 | keyPair: { 30 | publicKey: signalSignedPreKey.keyPair.pubKey, 31 | privateKey: signalSignedPreKey.keyPair.privKey, 32 | }, 33 | signature: signalSignedPreKey.signature, 34 | }); 35 | } 36 | 37 | public static async generateIdentityKey(): Promise { 38 | let signalIdentityKey = await SignalKeyHelper.generateIdentityKeyPair(); 39 | 40 | return new IdentityKey({ 41 | publicKey: signalIdentityKey.pubKey, 42 | privateKey: signalIdentityKey.privKey, 43 | }); 44 | } 45 | 46 | public static generateRegistrationId(): number { 47 | return SignalKeyHelper.generateRegistrationId(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/util/Utils.ts: -------------------------------------------------------------------------------- 1 | 2 | export default class Utils { 3 | public static removeHTML(text: string): string { 4 | return $('').html(text).text(); 5 | } 6 | 7 | public static escapeHTML(text: string): string { 8 | text = text.replace(/</g, '<').replace(/>/g, '>').replace(/&/g, '&'); 9 | return text.replace(/&/g, '&').replace(//g, '>'); 10 | } 11 | 12 | public static diffArray(newArray: any[], oldArray: any[]): { newValues: any[], deletedValues: any[] } { 13 | newArray = newArray || []; 14 | oldArray = oldArray || []; 15 | 16 | return { 17 | newValues: newArray.filter(id => (oldArray).indexOf(id) < 0), 18 | deletedValues: oldArray.filter(id => newArray.indexOf(id) < 0), 19 | } 20 | } 21 | 22 | public static isObject(candidate: any) { 23 | return !!candidate && candidate.constructor === Object; 24 | } 25 | 26 | public static mergeDeep(target: Object, ...sources: Object[]): Object { 27 | if (!sources.length) { 28 | return target; 29 | } 30 | if (!Utils.isObject(target)) { 31 | throw new Error('Target has to be an object'); 32 | } 33 | const source = sources.shift(); 34 | 35 | if (Utils.isObject(source)) { 36 | for (const key in source) { 37 | if (Utils.isObject(source[key])) { 38 | if (!target[key]) { 39 | Object.assign(target, { [key]: {} }); 40 | } 41 | 42 | Utils.mergeDeep(target[key], source[key]); 43 | } else { 44 | Object.assign(target, { [key]: source[key] }); 45 | } 46 | } 47 | } 48 | 49 | return Utils.mergeDeep(target, ...sources); 50 | } 51 | 52 | public static prettifyHex(hex: string) { 53 | return hex.replace(/(.{8})/g, '$1 ').replace(/ $/, ''); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /example/login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JSXC example application 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
        21 |

        Restricted Area simulated

        22 | 23 |

        This page simulates your protected area or your login target.

        24 | 25 |
        26 |

        You are not logged in! Maybe your password was wrong. Back

        27 |
        28 | 29 | 34 |
        35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /src/ui/dialogs/multiUserInvitation.ts: -------------------------------------------------------------------------------- 1 | import Dialog from '../Dialog' 2 | import MultiUserContact from '../../MultiUserContact' 3 | import JID from '../../JID' 4 | import Log from '../../util/Log' 5 | import Client from '../../Client' 6 | 7 | let multiUserInvitation = require('../../../template/multiUserInvitation.hbs'); 8 | 9 | export default function(type: 'direct' | 'mediated', from: string, room: string, reason: string, password: string, accountId: string) { 10 | let fromJid = new JID(from); 11 | let roomJid = new JID(room); 12 | let content = multiUserInvitation({ 13 | from, 14 | room, 15 | reason 16 | }); 17 | 18 | let dialog = new Dialog(content); 19 | let dom = dialog.open(); 20 | let account = Client.getAccountManager().getAccount(accountId); 21 | 22 | dom.find('form').on('submit', (ev) => { 23 | ev.preventDefault(); 24 | 25 | let multiUserContact = account.getContact(roomJid); 26 | 27 | if (!multiUserContact) { 28 | multiUserContact = new MultiUserContact(account, roomJid); 29 | multiUserContact.setAutoJoin(true); 30 | 31 | account.getContactManager().add(multiUserContact); 32 | } else if (multiUserContact.getType() !== MultiUserContact.TYPE) { 33 | Log.warn('Got normal contact. Abort.'); 34 | return; 35 | } 36 | 37 | if (!multiUserContact.getNickname()) { 38 | multiUserContact.setNickname(account.getJID().node); 39 | } 40 | 41 | multiUserContact.join(); 42 | 43 | let chatWindow = multiUserContact.getChatWindowController(); 44 | chatWindow.openProminently(); 45 | 46 | dialog.close(); 47 | }); 48 | 49 | dom.find('jsxc-js-close').click(() => { 50 | if (type === 'mediated') { 51 | account.getConnection().getMUCService().declineMediatedMultiUserInvitation(fromJid, roomJid); 52 | } 53 | }) 54 | } 55 | -------------------------------------------------------------------------------- /src/plugins/omemo/util/ArrayBuffer.ts: -------------------------------------------------------------------------------- 1 | import ByteBuffer = require('bytebuffer') 2 | 3 | let ArrayBufferUtils = { 4 | concat: (a: ArrayBuffer, b: ArrayBuffer) => ByteBuffer.concat([a, b]).toArrayBuffer(), 5 | 6 | decode: (a: ArrayBuffer): string => ByteBuffer.wrap(a).toUTF8(), 7 | 8 | encode: (s: string): ArrayBuffer => ByteBuffer.fromUTF8(s).toArrayBuffer(), 9 | 10 | toBase64: (a: ArrayBuffer): string => ByteBuffer.wrap(a).toBase64(), 11 | 12 | fromBase64: (s: string): ArrayBuffer => ByteBuffer.fromBase64(s.replace(/\s/g, '')).toArrayBuffer(), 13 | 14 | toString: (thing: ArrayBuffer | string): string => { 15 | if (typeof thing === 'string') { 16 | return thing; 17 | } 18 | 19 | return ByteBuffer.wrap(thing).toString('binary'); 20 | }, 21 | 22 | fromString: (thing: string): ArrayBuffer => { 23 | return ByteBuffer.wrap(thing, 'binary').toArrayBuffer(); 24 | }, 25 | 26 | toHex: (thing: ArrayBuffer | string): string => { 27 | if (typeof thing === 'undefined') { 28 | return ''; 29 | } 30 | 31 | return ByteBuffer.wrap(thing).toString('hex'); 32 | }, 33 | 34 | toPrettyHex: (thing: ArrayBuffer | string): string => { 35 | return ArrayBufferUtils.toHex(thing).replace(/(.{8})/g, '$1 ').replace(/ $/, ''); 36 | }, 37 | 38 | isEqual(a: ArrayBuffer | string, b: ArrayBuffer | string) { 39 | if (a === undefined || b === undefined) { 40 | return false; 41 | } 42 | 43 | a = ArrayBufferUtils.toString(a); 44 | b = ArrayBufferUtils.toString(b); 45 | 46 | if (Math.min(a.length, b.length) < 5) { 47 | throw new Error('a/b compare too short'); 48 | } 49 | 50 | return a === b; 51 | }, 52 | 53 | toArray: (a: ArrayBuffer): any[] => Array.apply([], new Uint8Array(a)), 54 | 55 | fromArray: (a: any[]): ArrayBuffer => new Uint8Array(a).buffer, 56 | } 57 | 58 | export default ArrayBufferUtils; 59 | --------------------------------------------------------------------------------