diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 00000000000..67fcd16e39c --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,28 @@ + + +## Wire for Web Version + + +## Browser Version + + +## Installed Browser Addons + + +## Operating System + +## What steps will reproduce the problem? +1. +2. +3. + +## What is the expected result? + + +## What happens instead? + + diff --git a/.github/issue_template.md b/.github/issue_template.md deleted file mode 100644 index 6201bcc6a89..00000000000 --- a/.github/issue_template.md +++ /dev/null @@ -1,19 +0,0 @@ -Wire for Web Version: -Browser Version: -Installed Browser Addons: -OS: - -What steps will reproduce the problem? -1. -2. -3. - -What is the expected result? - - -What happens instead? - - -Please provide any additional information below. Attach a screenshot if -possible. - diff --git a/app/font/Wire.ttf b/app/font/Wire.ttf index 55721bccb94..d01ddb45404 100755 Binary files a/app/font/Wire.ttf and b/app/font/Wire.ttf differ diff --git a/app/page/template/content/conversation/participants.htm b/app/page/template/content/conversation/participants.htm index 4839603351b..5c54650ae4f 100644 --- a/app/page/template/content/conversation/participants.htm +++ b/app/page/template/content/conversation/participants.htm @@ -62,10 +62,10 @@ diff --git a/app/page/template/content/preferences-account.htm b/app/page/template/content/preferences-account.htm index f18d6b8b26b..6726df158b4 100644 --- a/app/page/template/content/preferences-account.htm +++ b/app/page/template/content/preferences-account.htm @@ -58,7 +58,7 @@ - +
diff --git a/app/page/template/content/preferences-options.htm b/app/page/template/content/preferences-options.htm index bd83b500d37..9c68235ca6c 100644 --- a/app/page/template/content/preferences-options.htm +++ b/app/page/template/content/preferences-options.htm @@ -77,7 +77,7 @@
- +
diff --git a/app/page/template/list/archive.htm b/app/page/template/list/archive.htm index 63d67e87913..044adedcb5c 100644 --- a/app/page/template/list/archive.htm +++ b/app/page/template/list/archive.htm @@ -1,7 +1,7 @@
- +
    diff --git a/app/page/template/list/preferences.htm b/app/page/template/list/preferences.htm index fb5353bf73e..4737ba78282 100644 --- a/app/page/template/list/preferences.htm +++ b/app/page/template/list/preferences.htm @@ -1,7 +1,7 @@
    - +
      @@ -23,7 +23,7 @@
      - +
    • diff --git a/app/page/template/list/start-ui.htm b/app/page/template/list/start-ui.htm index f65d78c740c..2438107fd9d 100644 --- a/app/page/template/list/start-ui.htm +++ b/app/page/template/list/start-ui.htm @@ -1,11 +1,12 @@
      +
      @@ -27,8 +28,8 @@
      -
      -
      +
      +
      @@ -129,7 +130,8 @@

      - + +

      diff --git a/app/page/template/warning.htm b/app/page/template/warning.htm index 12ab0e8d405..c9ccca64dc1 100644 --- a/app/page/template/warning.htm +++ b/app/page/template/warning.htm @@ -89,13 +89,13 @@
       
      - +
       
      - +
      @@ -112,13 +112,13 @@
      - +
       
      - +
      diff --git a/app/script/calling/SDPMapper.js b/app/script/calling/SDPMapper.js index 462f0c63a11..f445c27840e 100644 --- a/app/script/calling/SDPMapper.js +++ b/app/script/calling/SDPMapper.js @@ -85,7 +85,7 @@ z.calling.SDPMapper = { sdp_lines.push(sdp_line); const browser_string = `${z.util.Environment.browser.name} ${z.util.Environment.browser.version}`; - if (z.util.Environment.electron) { + if (z.util.Environment.desktop) { outline = `a=tool:electron ${z.util.Environment.version()} ${z.util.Environment.version(false)} (${browser_string})`; } else { outline = `a=tool:webapp ${z.util.Environment.version(false)} (${browser_string})`; diff --git a/app/script/calling/entities/Flow.js b/app/script/calling/entities/Flow.js index 7e45504abd8..fe1fee56791 100644 --- a/app/script/calling/entities/Flow.js +++ b/app/script/calling/entities/Flow.js @@ -63,7 +63,8 @@ z.calling.entities.Flow = class Flow { this.remote_client_id = undefined; this.remote_user = this.participant_et.user; this.remote_user_id = this.remote_user.id; - this.self_user_id = this.call_et.self_user_id; + this.self_user = this.call_et.self_user; + this.self_user_id = this.self_user.id; // Telemetry this.telemetry = new z.telemetry.calling.FlowTelemetry(this.id, this.remote_user_id, this.call_et, timings); @@ -1048,7 +1049,10 @@ z.calling.entities.Flow = class Flow { * @returns {boolean} False if we locally needed to switch sides */ _solve_colliding_states(force_renegotiation = false) { - if (this.self_user_id < this.remote_user_id || force_renegotiation) { + this.logger.debug(`Solving state collision: Self user ID '${this.self_user_id}', remote user ID '${this.remote_user_id}', force_renegotiation '${force_renegotiation}'`); + + const self_user_id_looses = this.self_user_id < this.remote_user_id; + if (self_user_id_looses || force_renegotiation) { this.logger.warn(`We need to switch SDP state of flow with '${this.remote_user.name()}' to answer.`); this.restart_negotiation(z.calling.enum.SDP_NEGOTIATION_MODE.STATE_COLLISION, true); diff --git a/app/script/client/ClientRepository.js b/app/script/client/ClientRepository.js index cf598de2cd9..cea2cf83e48 100644 --- a/app/script/client/ClientRepository.js +++ b/app/script/client/ClientRepository.js @@ -339,7 +339,7 @@ z.client.ClientRepository = class ClientRepository { let device_model = platform.name; - if (z.util.Environment.electron) { + if (z.util.Environment.desktop) { let identifier; if (z.util.Environment.os.mac) { identifier = z.string.wire_macos; diff --git a/app/script/components/userInput.js b/app/script/components/userInput.js index 973049c21ad..03a948a3b20 100644 --- a/app/script/components/userInput.js +++ b/app/script/components/userInput.js @@ -30,7 +30,6 @@ z.components.UserListInputViewModel = class UserListInputViewModel { this.selected = params.selected || ko.observableArray([]); this.placeholder = params.placeholder; this.on_enter = params.enter; - this.on_close = params.close; this.element = component_info.element; this.input_element = $(this.element).find('.search-input'); @@ -66,17 +65,16 @@ z.components.UserListInputViewModel = class UserListInputViewModel { }; ko.components.register('user-input', { - template: `
      -
      +
      + - -
      +
      diff --git a/app/script/config.js b/app/script/config.js index 037f840ea1b..024ab088ff4 100644 --- a/app/script/config.js +++ b/app/script/config.js @@ -72,9 +72,13 @@ window.z.config = { // 15 megabyte image upload limit MAXIMUM_IMAGE_FILE_SIZE: 15 * 1024 * 1024, - // Maximum characters per message + // Maximum characters per sent message MAXIMUM_MESSAGE_LENGTH: 8000, + // Maximum characters per received message + // Encryption is approx. +40% of the original payload so let's round it at +50% + MAXIMUM_MESSAGE_LENGTH_RECEIVING: 12000 * 1.5, + // bigger requests will be split in chunks with a maximum size as defined MAXIMUM_USERS_PER_REQUEST: 200, diff --git a/app/script/conversation/ConversationRepository.js b/app/script/conversation/ConversationRepository.js index c5eadc3fcd1..98e180d5650 100644 --- a/app/script/conversation/ConversationRepository.js +++ b/app/script/conversation/ConversationRepository.js @@ -1297,8 +1297,8 @@ z.conversation.ConversationRepository = class ConversationRepository { }) .catch((error) => { this.logger.error(`Error (${error.label}): ${error.message}`); - error = new Error('Failed to update last read timestamp'); - Raygun.send(error, {label: error.label, message: error.message}); + const raygun_error = new Error('Failed to update last read timestamp'); + Raygun.send(raygun_error, {label: error.label, message: error.message}); }); } } @@ -1603,7 +1603,7 @@ z.conversation.ConversationRepository = class ConversationRepository { return this._send_and_inject_generic_message(conversation_et, generic_message, false) .then(() => { this._track_edit_message(conversation_et, original_message_et); - if (z.util.Environment.electron) { + if (z.util.Environment.desktop) { return this.send_link_preview(message, conversation_et, generic_message); } }) @@ -1708,7 +1708,7 @@ z.conversation.ConversationRepository = class ConversationRepository { send_text_with_link_preview(message, conversation_et) { return this.send_text(message, conversation_et) .then((generic_message) => { - if (z.util.Environment.electron) { + if (z.util.Environment.desktop) { return this.send_link_preview(message, conversation_et, generic_message); } }) diff --git a/app/script/conversation/ConversationService.js b/app/script/conversation/ConversationService.js index 7d9dab4da17..009b588cd68 100644 --- a/app/script/conversation/ConversationService.js +++ b/app/script/conversation/ConversationService.js @@ -539,16 +539,19 @@ z.conversation.ConversationService = class ConversationService { update_asset_as_uploaded_in_db(primary_key, asset_data) { return this.storage_service.load(z.storage.StorageService.OBJECT_STORE.EVENTS, primary_key) .then((record) => { - record.data.id = asset_data.id; - record.data.otr_key = asset_data.otr_key; - record.data.sha256 = asset_data.sha256; - record.data.key = asset_data.key; - record.data.token = asset_data.token; - record.data.status = z.assets.AssetTransferState.UPLOADED; - return this.storage_service.update(z.storage.StorageService.OBJECT_STORE.EVENTS, primary_key, record); - }) - .then(() => { - this.logger.info('Updated asset message_et (uploaded)', primary_key); + if (record) { + record.data.id = asset_data.id; + record.data.key = asset_data.key; + record.data.otr_key = asset_data.otr_key; + record.data.sha256 = asset_data.sha256; + record.data.status = z.assets.AssetTransferState.UPLOADED; + record.data.token = asset_data.token; + + return this.storage_service.update(z.storage.StorageService.OBJECT_STORE.EVENTS, primary_key, record) + .then(() => this.logger.info('Updated asset message_et (uploaded)', primary_key)); + } + + this.logger.warn('Did not find message to update asset (uploaded)', primary_key); }); } @@ -562,15 +565,18 @@ z.conversation.ConversationService = class ConversationService { update_asset_preview_in_db(primary_key, asset_data) { return this.storage_service.load(z.storage.StorageService.OBJECT_STORE.EVENTS, primary_key) .then((record) => { - record.data.preview_id = asset_data.id; - record.data.preview_otr_key = asset_data.otr_key; - record.data.preview_sha256 = asset_data.sha256; - record.data.preview_key = asset_data.key; - record.data.preview_token = asset_data.token; - return this.storage_service.update(z.storage.StorageService.OBJECT_STORE.EVENTS, primary_key, record); - }) - .then(() => { - this.logger.info('Updated asset message_et (preview)', primary_key); + if (record) { + record.data.preview_id = asset_data.id; + record.data.preview_key = asset_data.key; + record.data.preview_otr_key = asset_data.otr_key; + record.data.preview_sha256 = asset_data.sha256; + record.data.preview_token = asset_data.token; + + return this.storage_service.update(z.storage.StorageService.OBJECT_STORE.EVENTS, primary_key, record) + .then(() => this.logger.info('Updated asset message_et (preview)', primary_key)); + } + + this.logger.warn('Did not find message to update asset (preview)', primary_key); }); } @@ -584,12 +590,15 @@ z.conversation.ConversationService = class ConversationService { update_asset_as_failed_in_db(primary_key, reason) { return this.storage_service.load(z.storage.StorageService.OBJECT_STORE.EVENTS, primary_key) .then((record) => { - record.data.status = z.assets.AssetTransferState.UPLOAD_FAILED; - record.data.reason = reason; - return this.storage_service.update(z.storage.StorageService.OBJECT_STORE.EVENTS, primary_key, record); - }) - .then(() => { - this.logger.info('Updated asset message_et (failed)', primary_key); + if (record) { + record.data.reason = reason; + record.data.status = z.assets.AssetTransferState.UPLOAD_FAILED; + + return this.storage_service.update(z.storage.StorageService.OBJECT_STORE.EVENTS, primary_key, record) + .then(() => this.logger.info('Updated asset message_et (failed)', primary_key)); + } + + this.logger.warn('Did not find message to update asset (failed)', primary_key); }); } diff --git a/app/script/conversation/EventBuilder.js b/app/script/conversation/EventBuilder.js index 0b4b00580db..0d3723ab8fe 100644 --- a/app/script/conversation/EventBuilder.js +++ b/app/script/conversation/EventBuilder.js @@ -115,6 +115,20 @@ z.conversation.EventBuilder = (function() { }; }; + const _build_incoming_message_too_big = (event, message_error, error_code) => { + const {conversation: conversation_id, data: event_data, from, time} = event; + + return { + conversation: conversation_id, + error: `${message_error.message} (${event_data.sender})`, + error_code: `${error_code} (${event_data.sender})`, + from: from, + id: z.util.create_random_uuid(), + time: time, + type: z.event.Client.CONVERSATION.INCOMING_MESSAGE_TOO_BIG, + }; + }; + const _build_voice_channel_activate = (call_message_et) => { const {conversation_id, user_id, time} = call_message_et; @@ -149,6 +163,7 @@ z.conversation.EventBuilder = (function() { build_calling: _build_calling, build_degraded: _build_degraded, build_delete: _build_delete, + build_incoming_message_too_big: _build_incoming_message_too_big, build_missed: _build_missed, build_team_member_leave: _build_team_member_leave, build_unable_to_decrypt: _build_unable_to_decrypt, diff --git a/app/script/conversation/EventMapper.js b/app/script/conversation/EventMapper.js index 7935555b09f..aa3b0468af9 100644 --- a/app/script/conversation/EventMapper.js +++ b/app/script/conversation/EventMapper.js @@ -123,6 +123,7 @@ z.conversation.EventMapper = class EventMapper { message_et = this._map_event_verification(event); break; case z.event.Client.CONVERSATION.UNABLE_TO_DECRYPT: + case z.event.Client.CONVERSATION.MESSAGE_TOO_BIG: message_et = this._map_event_unable_to_decrypt(event); break; default: diff --git a/app/script/cryptography/CryptographyRepository.js b/app/script/cryptography/CryptographyRepository.js index d319d9e9984..4ea68a6d901 100644 --- a/app/script/cryptography/CryptographyRepository.js +++ b/app/script/cryptography/CryptographyRepository.js @@ -248,6 +248,12 @@ z.cryptography.CryptographyRepository = class CryptographyRepository { return Promise.reject(new z.cryptography.CryptographyError(z.cryptography.CryptographyError.TYPE.NO_DATA_CONTENT)); } + // Check the length of the message + if (typeof event_data.text === 'string' && (event_data.text.length > z.config.MAXIMUM_MESSAGE_LENGTH_RECEIVING)) { + const decryption_error = new Proteus.errors.DecryptError.InvalidMessage('The received message was too big.', 300); + return Promise.resolve(z.conversation.EventBuilder.build_incoming_message_too_big(event, decryption_error, decryption_error.code)); + } + if (event_data.text === CryptographyRepository.REMOTE_ENCRYPTION_FAILURE) { const decryption_error = new Proteus.errors.DecryptError.InvalidMessage('The sending client couldn\'t encrypt a message for our client.'); return Promise.resolve(this._handle_decryption_failure(decryption_error, event)); diff --git a/app/script/event/Client.js b/app/script/event/Client.js index 56e8cbf91ee..8a926338cc1 100644 --- a/app/script/event/Client.js +++ b/app/script/event/Client.js @@ -33,6 +33,7 @@ z.event.Client = { ASSET_UPLOAD_FAILED: 'conversation.asset-upload-failed', CONFIRMATION: 'conversation.confirmation', DELETE_EVERYWHERE: 'conversation.delete-everywhere', + INCOMING_MESSAGE_TOO_BIG: 'conversation.incoming-message-too-big', LOCATION: 'conversation.location', MESSAGE_DELETE: 'conversation.message-delete', MESSAGE_HIDDEN: 'conversation.message-hidden', diff --git a/app/script/event/EventTypeHandling.js b/app/script/event/EventTypeHandling.js index cb48ebcda88..6714317d166 100644 --- a/app/script/event/EventTypeHandling.js +++ b/app/script/event/EventTypeHandling.js @@ -55,6 +55,7 @@ z.event.EventTypeHandling = { z.event.Client.CONVERSATION.MISSED_MESSAGES, z.event.Client.CONVERSATION.TEAM_MEMBER_LEAVE, z.event.Client.CONVERSATION.UNABLE_TO_DECRYPT, + z.event.Client.CONVERSATION.INCOMING_MESSAGE_TOO_BIG, z.event.Client.CONVERSATION.VERIFICATION, ], }; diff --git a/app/script/links/LinkPreviewRepository.js b/app/script/links/LinkPreviewRepository.js index 991ed6eaaf5..1ba7af495e1 100644 --- a/app/script/links/LinkPreviewRepository.js +++ b/app/script/links/LinkPreviewRepository.js @@ -46,7 +46,7 @@ z.links.LinkPreviewRepository = class LinkPreviewRepository { * @returns {Promise} Resolves with link preview proto message */ get_link_preview_from_string(string) { - if (this.should_send_previews && z.util.Environment.electron) { + if (this.should_send_previews && z.util.Environment.desktop) { return Promise.resolve() .then(() => { const data = z.links.LinkPreviewHelpers.get_first_link_with_offset(string); diff --git a/app/script/localization/webapp.js b/app/script/localization/webapp.js index b057b76bb92..b98009b336b 100644 --- a/app/script/localization/webapp.js +++ b/app/script/localization/webapp.js @@ -112,6 +112,7 @@ z.string.auth_error_name_short = 'Enter a name with at least 2 characters'; z.string.auth_error_offline = 'No Internet connection'; z.string.auth_error_password_short = 'Choose a password with at least 8 characters.'; z.string.auth_error_password_wrong = 'Wrong password. Please try again.'; +z.string.auth_error_phone_number_budget = 'You logged in too often. Try again later.'; z.string.auth_error_phone_number_forbidden = 'Sorry. This phone number is forbidden.'; z.string.auth_error_phone_number_invalid = 'Invalid Phone Number'; z.string.auth_error_phone_number_unknown = 'Unknown Phone Number'; @@ -357,18 +358,17 @@ z.string.extensions_giphy_random = 'Random'; // People View z.string.search_open = 'Open'; z.string.search_open_group = 'Create Group'; -z.string.people_confirm_label = 'Add to conversation'; +z.string.people_confirm_label = 'Add people to group'; z.string.people_people = '{{number}} People'; z.string.people_search_placeholder = 'Search by name'; z.string.people_everyone_participates = 'Everyone you’re\nconnected to is already in\nthis conversation.'; z.string.people_no_matches = 'No matching results.\nTry entering a different name.'; -z.string.people_invite = 'Invite people'; -z.string.people_share = 'Share Contacts'; -z.string.people_bring_your_friends = 'Bring your Friends to Wire'; +z.string.people_invite = 'Invite people to join Wire'; z.string.people_invite_detail = 'Sharing your contacts helps you connect with others. We anonymize all the information and do not share it with anyone else.'; z.string.people_invite_button_contacts = 'From Contacts'; z.string.people_invite_button_gmail = 'From Gmail'; z.string.people_invite_headline = 'Bring your friends'; +z.string.people_share = 'Share Contacts'; z.string.people_tabs_details = 'Details'; z.string.people_tabs_devices = 'Devices'; z.string.people_tabs_devices_headline = 'Wire gives every device a unique fingerprint. Compare them with {{user}} and verify your conversation.'; diff --git a/app/script/main/app.js b/app/script/main/app.js index fbc2d01c8f6..fbf12682651 100644 --- a/app/script/main/app.js +++ b/app/script/main/app.js @@ -25,7 +25,11 @@ window.z.main = z.main || {}; z.main.App = class App { static get CONFIG() { return { - COOKIE_NAME: 'app_opened', + TABS_CHECK: { + COOKIE_NAME: 'app_opened', + COOKIE_TIMEOUT: 5 * 60 * 1000, + RENEWAL_THRESHOLD: 15 * 1000, + }, }; } @@ -199,7 +203,7 @@ z.main.App = class App { init_app(is_reload = this._is_reload()) { z.util.check_indexed_db() .then(() => this._check_single_instance()) - .then(() => this._load_access_token(is_reload)) + .then(() => this._load_access_token()) .then(() => { this.view.loading.update_progress(2.5, z.string.init_received_access_token); this.telemetry.time_step(z.telemetry.app_init.AppInitTimingsStep.RECEIVED_ACCESS_TOKEN); @@ -327,7 +331,7 @@ z.main.App = class App { _app_init_failure(error, is_reload) { let log_message = `Could not initialize app version '${z.util.Environment.version(false)}'`; - if (z.util.Environment.electron) { + if (z.util.Environment.desktop) { log_message = `${log_message} - Electron '${platform.os.family}' '${z.util.Environment.version()}'`; } this.logger.info(log_message, {error}); @@ -418,13 +422,16 @@ z.main.App = class App { * @returns {Promise} Resolves when page is the first tab */ _check_single_instance() { - const cookie_name = App.CONFIG.COOKIE_NAME; - if (Cookies.get(cookie_name)) { - return Promise.reject(new z.auth.AuthError(z.auth.AuthError.TYPE.MULTIPLE_TABS)); + if (!z.util.Environment.electron) { + const cookie_name = App.CONFIG.TABS_CHECK.COOKIE_NAME; + if (Cookies.get(cookie_name)) { + return Promise.reject(new z.auth.AuthError(z.auth.AuthError.TYPE.MULTIPLE_TABS)); + } + + this._set_single_instance_cookie(); + $(window).on('beforeunload', () => Cookies.remove(cookie_name)); } - Cookies.set(cookie_name, true); - $(window).on('unload', () => Cookies.remove(cookie_name)); return Promise.resolve(); } @@ -486,6 +493,17 @@ z.main.App = class App { return token_promise; } + /** + * Set the cookie to verify we are running a single instace tab. + * @returns {undefined} No return value + */ + _set_single_instance_cookie() { + const cookie_timeout = new Date(Date.now() + App.CONFIG.TABS_CHECK.COOKIE_TIMEOUT); + Cookies.set(App.CONFIG.TABS_CHECK.COOKIE_NAME, true, {expires: cookie_timeout}); + + const renewal_timeout = App.CONFIG.TABS_CHECK.COOKIE_TIMEOUT - App.CONFIG.TABS_CHECK.RENEWAL_THRESHOLD; + window.setTimeout(() => this._set_single_instance_cookie(), renewal_timeout); + } /** * Hide the loading spinner and show the application UI. @@ -613,7 +631,7 @@ z.main.App = class App { * @returns {undefined} No return value */ refresh() { - if (z.util.Environment.electron) { + if (z.util.Environment.desktop) { amplify.publish(z.event.WebApp.LIFECYCLE.RESTART, this.update_source); } if (this.update_source === z.announce.UPDATE_SOURCE.WEBAPP) { diff --git a/app/script/media/MediaEmbeds.js b/app/script/media/MediaEmbeds.js index 8e927dac4d6..a68f8a3621c 100644 --- a/app/script/media/MediaEmbeds.js +++ b/app/script/media/MediaEmbeds.js @@ -49,7 +49,7 @@ z.media.MediaEmbeds = (function() { options.class = 'iframe-container'; } - if (z.util.Environment.electron) { + if (z.util.Environment.desktop) { options.allowfullscreen = ''; } diff --git a/app/script/media/MediaStreamHandler.js b/app/script/media/MediaStreamHandler.js index ae3c50ceb4a..221a2536674 100644 --- a/app/script/media/MediaStreamHandler.js +++ b/app/script/media/MediaStreamHandler.js @@ -411,7 +411,9 @@ z.media.MediaStreamHandler = class MediaStreamHandler { * @returns {undefined} No return value */ _hide_permission_request_hint(media_type) { - if (z.util.Environment.electron) return; + if (z.util.Environment.electron) { + return; + } switch (media_type) { case z.media.MediaType.AUDIO: @@ -543,7 +545,9 @@ z.media.MediaStreamHandler = class MediaStreamHandler { * @returns {undefined} No return value */ _show_permission_request_hint(media_type) { - if (z.util.Environment.electron) return; + if (z.util.Environment.electron) { + return; + } switch (media_type) { case z.media.MediaType.AUDIO: diff --git a/app/script/service/BackendClient.js b/app/script/service/BackendClient.js index 6a47e5a9ec9..87dc5891ca6 100644 --- a/app/script/service/BackendClient.js +++ b/app/script/service/BackendClient.js @@ -48,7 +48,6 @@ z.service.BackendClient = class BackendClient { static get IGNORED_BACKEND_ERRORS() { return [ z.service.BackendClientError.STATUS_CODE.BAD_GATEWAY, - z.service.BackendClientError.STATUS_CODE.BAD_REQUEST, z.service.BackendClientError.STATUS_CODE.CONFLICT, z.service.BackendClientError.STATUS_CODE.CONNECTIVITY_PROBLEM, z.service.BackendClientError.STATUS_CODE.INTERNAL_SERVER_ERROR, @@ -66,6 +65,7 @@ z.service.BackendClient = class BackendClient { z.service.BackendClientError.LABEL.PASSWORD_EXISTS, z.service.BackendClientError.LABEL.TOO_MANY_CLIENTS, z.service.BackendClientError.LABEL.TOO_MANY_MEMBERS, + z.service.BackendClientError.LABEL.UNKNOWN_CLIENT, ]; } @@ -276,13 +276,12 @@ z.service.BackendClient = class BackendClient { xhrFields: config.xhrFields, }) .done((data, textStatus, {wire: wire_request}) => { - if (wire_request) { - this.logger.debug(this.logger.levels.OFF, `Server Response '${wire_request.request_id}' from '${config.url}':`, data); - } + const request_id = wire_request ? wire_request.request : 'ID not set'; + this.logger.debug(this.logger.levels.OFF, `Server response to '${config.type}' request '${config.url}' - '${request_id}':`, data); resolve(data); }) - .fail(({responseJSON: response, status: status_code}) => { + .fail(({responseJSON: response, status: status_code, wire: wire_request}) => { switch (status_code) { case z.service.BackendClientError.STATUS_CODE.CONNECTIVITY_PROBLEM: { this.request_queue.pause(); @@ -301,10 +300,19 @@ z.service.BackendClient = class BackendClient { case z.service.BackendClientError.STATUS_CODE.FORBIDDEN: { if (response) { - if (BackendClient.IGNORED_BACKEND_LABELS.includes(response.label)) { - this.logger.warn(`Server request failed: ${response.label}`); + const error_label = response.label; + const error_message = `Server request forbidden: ${error_label}`; + + if (BackendClient.IGNORED_BACKEND_LABELS.includes(error_label)) { + this.logger.warn(error_message); } else { - Raygun.send(new Error(`Server request failed: ${response.label}`)); + const request_id = wire_request ? wire_request.request_id : undefined; + const custom_data = { + endpoint: config.url, + request_id: request_id, + }; + + Raygun.send(new Error(error_message), custom_data); } } break; @@ -325,12 +333,20 @@ z.service.BackendClient = class BackendClient { this._push_to_request_queue(config, z.service.RequestQueueBlockedState.ACCESS_TOKEN_REFRESH) .then(resolve) .catch(reject); - return amplify.publish(z.event.WebApp.CONNECTION.ACCESS_TOKEN.RENEW, z.auth.AuthRepository.ACCESS_TOKEN_TRIGGER.UNAUTHORIZED_REQUEST); + + const trigger = z.auth.AuthRepository.ACCESS_TOKEN_TRIGGER.UNAUTHORIZED_REQUEST; + return amplify.publish(z.event.WebApp.CONNECTION.ACCESS_TOKEN.RENEW, trigger); } default: { if (!BackendClient.IGNORED_BACKEND_ERRORS.includes(status_code)) { - Raygun.send(new Error(`Server request failed: ${status_code}`)); + const request_id = wire_request ? wire_request.request_id : undefined; + const custom_data = { + endpoint: config.url, + request_id: request_id, + }; + + Raygun.send(new Error(`Server request failed: ${status_code}`), custom_data); } } } diff --git a/app/script/service/BackendClientError.js b/app/script/service/BackendClientError.js index 584d4b17bb9..115db87b83b 100644 --- a/app/script/service/BackendClientError.js +++ b/app/script/service/BackendClientError.js @@ -56,6 +56,7 @@ z.service.BackendClientError = class BackendClientError extends Error { PASSWORD_EXISTS: 'password-exists', PENDING_ACTIVATION: 'pending-activation', PENDING_LOGIN: 'pending-login', + PHONE_BUDGET_EXHAUSTED: 'phone-budget-exhausted', TOO_MANY_CLIENTS: 'too-many-clients', TOO_MANY_MEMBERS: 'too-many-members', UNAUTHORIZED: 'unauthorized', diff --git a/app/script/telemetry/app_init/AppInitStatistics.js b/app/script/telemetry/app_init/AppInitStatistics.js index c1ac594c667..ed5625f7397 100644 --- a/app/script/telemetry/app_init/AppInitStatistics.js +++ b/app/script/telemetry/app_init/AppInitStatistics.js @@ -24,6 +24,13 @@ window.z.telemetry = z.telemetry || {}; window.z.telemetry.app_init = z.telemetry.app_init || {}; z.telemetry.app_init.AppInitStatistics = class AppInitStatistics { + static get CONFIG() { + return { + LOG_LENGTH_KEY: 17, + LOG_LENGTH_VALUE: 11, + }; + } + constructor() { this.logger = new z.util.Logger('z.telemetry.app_init.AppInitStatistics', z.config.LOGGER.OPTIONS); @@ -63,8 +70,10 @@ z.telemetry.app_init.AppInitStatistics = class AppInitStatistics { if (this.hasOwnProperty(key)) { const value = this[key]; if (_.isNumber(value) || _.isString(value)) { - const placeholder_key = Array.from(Math.max(17 - key.length, 1)).join(' '); - const placeholder_value = Array.from(Math.max(11 - value.toString().length, 1)).join(' '); + const placeholder_key_length = Math.max(AppInitStatistics.CONFIG.LOG_LENGTH_KEY - key.length, 1); + const placeholder_key = new Array(placeholder_key_length).join(' '); + const placeholder_value_length = Math.max(AppInitStatistics.CONFIG.LOG_LENGTH_VALUE - value.toString().length, 1); + const placeholder_value = new Array(placeholder_value_length).join(' '); this.logger.info(`${placeholder_key}'${key}':${placeholder_value}${value}`); } diff --git a/app/script/tracking/EventTrackingRepository.js b/app/script/tracking/EventTrackingRepository.js index 647933b6c3f..f4b1e519507 100644 --- a/app/script/tracking/EventTrackingRepository.js +++ b/app/script/tracking/EventTrackingRepository.js @@ -352,7 +352,7 @@ z.tracking.EventTrackingRepository = class EventTrackingRepository { if (!z.util.Environment.frontend.is_localhost()) { Raygun.setVersion(z.util.Environment.version(false)); } - if (z.util.Environment.electron) { + if (z.util.Environment.desktop) { Raygun.withCustomData({electron_version: z.util.Environment.version(true)}); } Raygun.onBeforeSend(this._check_error_payload.bind(this)); diff --git a/app/script/tracking/Helpers.js b/app/script/tracking/Helpers.js index 4e23868c498..75fa70f3866 100644 --- a/app/script/tracking/Helpers.js +++ b/app/script/tracking/Helpers.js @@ -66,7 +66,7 @@ z.tracking.helpers = { * @returns {z.tracking.attribute.PlatformType} Mapped platform type */ get_platform() { - if (z.util.Environment.electron) { + if (z.util.Environment.desktop) { if (z.util.Environment.os.win) { return z.tracking.attribute.PlatformType.DESKTOP_WINDOWS; } diff --git a/app/script/ui/Shortcut.js b/app/script/ui/Shortcut.js index 50dc9d85d6e..64c9b44347b 100644 --- a/app/script/ui/Shortcut.js +++ b/app/script/ui/Shortcut.js @@ -215,7 +215,7 @@ window.z.ui = z.ui || {}; } function get_shortcut(shortcut_name) { - const platform = z.util.Environment.electron ? 'electron' : 'webapp'; + const platform = z.util.Environment.desktop ? 'electron' : 'webapp'; const platform_shortcuts = shortcut_map[shortcut_name].shortcut[platform]; return z.util.Environment.os.mac ? platform_shortcuts.macos : platform_shortcuts.pc; } @@ -233,7 +233,7 @@ window.z.ui = z.ui || {}; function _init() { for (const shortcut in shortcut_map) { const data = shortcut_map[shortcut]; - if (z.util.Environment.electron && shortcut_map[shortcut].shortcut.electron.menu) { + if (z.util.Environment.desktop && shortcut_map[shortcut].shortcut.electron.menu) { continue; } _register_event(get_shortcut(shortcut), data['event']); diff --git a/app/script/util/Emoji.js b/app/script/util/Emoji.js index b8ca86f8eb6..e070cb7f5c8 100644 --- a/app/script/util/Emoji.js +++ b/app/script/util/Emoji.js @@ -22,15 +22,32 @@ window.z = window.z || {}; window.z.util = z.util || {}; +// http://www.unicode.org/Public/emoji/5.0/emoji-data.txt +// This is the exact copy of unicode-range definition for `emoji` font in CSS. +const EMOJI_UNICODE_RANGES = 'U+1F004, U+1F0CF, U+1F170-1F171, U+1F17E, U+1F17F, U+1F18E, U+1F191-1F19A, U+1F1E6-1F1FF, U+1F201, U+1F201-1F202, U+1F21A, U+1F22F, U+1F232-1F236, U+1F232-1F23A, U+1F238-1F23A, U+1F250-1F251, U+1F300-1F320, U+1F321, U+1F324-1F32C, U+1F32D-1F32F, U+1F330-1F335, U+1F336, U+1F337-1F37C, U+1F37D, U+1F37E-1F37F, U+1F380-1F393, U+1F385, U+1F396-1F397, U+1F399-1F39B, U+1F39E-1F39F, U+1F3A0-1F3C4, U+1F3C2-1F3C4, U+1F3C5, U+1F3C6-1F3CA, U+1F3C7, U+1F3CA, U+1F3CB-1F3CC, U+1F3CB-1F3CE, U+1F3CF-1F3D3, U+1F3D4-1F3DF, U+1F3E0-1F3F0, U+1F3F3-1F3F5, U+1F3F4, U+1F3F7, U+1F3F8-1F3FF, U+1F3FB-1F3FF, U+1F400-1F43E, U+1F43F, U+1F440, U+1F441, U+1F442-1F443, U+1F442-1F4F7, U+1F446-1F450, U+1F466-1F469, U+1F46E, U+1F470-1F478, U+1F47C, U+1F481-1F483, U+1F485-1F487, U+1F4AA, U+1F4F8, U+1F4F9-1F4FC, U+1F4FD, U+1F4FF, U+1F500-1F53D, U+1F549-1F54A, U+1F54B-1F54E, U+1F550-1F567, U+1F56F-1F570, U+1F573-1F579, U+1F574-1F575, U+1F57A, U+1F587, U+1F58A-1F58D, U+1F590, U+1F595-1F596, U+1F5A4, U+1F5A5, U+1F5A8, U+1F5B1-1F5B2, U+1F5BC, U+1F5C2-1F5C4, U+1F5D1-1F5D3, U+1F5DC-1F5DE, U+1F5E1, U+1F5E3, U+1F5E8, U+1F5EF, U+1F5F3, U+1F5FA, U+1F5FB-1F5FF, U+1F600, U+1F601-1F610, U+1F611, U+1F612-1F614, U+1F615, U+1F616, U+1F617, U+1F618, U+1F619, U+1F61A, U+1F61B, U+1F61C-1F61E, U+1F61F, U+1F620-1F625, U+1F626-1F627, U+1F628-1F62B, U+1F62C, U+1F62D, U+1F62E-1F62F, U+1F630-1F633, U+1F634, U+1F635-1F640, U+1F641-1F642, U+1F643-1F644, U+1F645-1F647, U+1F645-1F64F, U+1F64B-1F64F, U+1F680-1F6C5, U+1F6A3, U+1F6B4-1F6B6, U+1F6C0, U+1F6CB-1F6CF, U+1F6CC, U+1F6D0, U+1F6D1-1F6D2, U+1F6E0-1F6E5, U+1F6E9, U+1F6EB-1F6EC, U+1F6F0, U+1F6F3, U+1F6F4-1F6F6, U+1F6F7-1F6F8, U+1F910-1F918, U+1F918, U+1F919-1F91C, U+1F919-1F91E, U+1F91E, U+1F91F, U+1F920-1F927, U+1F926, U+1F928-1F92F, U+1F930, U+1F931-1F932, U+1F933-1F939, U+1F933-1F93A, U+1F93C-1F93E, U+1F93D-1F93E, U+1F940-1F945, U+1F947-1F94B, U+1F94C, U+1F950-1F95E, U+1F95F-1F96B, U+1F980-1F984, U+1F985-1F991, U+1F992-1F997, U+1F9C0, U+1F9D0-1F9E6, U+1F9D1-1F9DD, U+203C, U+2049, U+2122, U+2139, U+2194-2199, U+21A9-21AA, U+231A-231B, U+2328, U+23CF, U+23E9-23EC, U+23E9-23F3, U+23F0, U+23F3, U+23F8-23FA, U+24C2, U+25AA-25AB, U+25B6, U+25C0, U+25FB-25FE, U+25FD-25FE, U+2600-2604, U+260E, U+2611, U+2614-2615, U+2618, U+261D, U+2620, U+2622-2623, U+2626, U+262A, U+262E-262F, U+2638-263A, U+2640, U+2642, U+2648-2653, U+2660, U+2663, U+2665-2666, U+2668, U+267B, U+267F, U+2692-2697, U+2693, U+2699, U+269B-269C, U+26A0-26A1, U+26A1, U+26AA-26AB, U+26B0-26B1, U+26BD-26BE, U+26C4-26C5, U+26C8, U+26CE, U+26CF, U+26D1, U+26D3-26D4, U+26D4, U+26E9-26EA, U+26EA, U+26F0-26F5, U+26F2-26F3, U+26F5, U+26F7-26FA, U+26F9, U+26FA, U+26FD, U+2702, U+2705, U+2708-2709, U+270A-270B, U+270C-270D, U+270F, U+2712, U+2714, U+2716, U+271D, U+2721, U+2728, U+2733-2734, U+2744, U+2747, U+274C, U+274E, U+2753-2755, U+2757, U+2763-2764, U+2795-2797, U+27A1, U+27B0, U+27BF, U+2934-2935, U+2B05-2B07, U+2B1B-2B1C, U+2B50, U+2B55, U+3030, U+303D, U+3297, U+3299' + .replace(/U\+/g, '') + .split(', ') + .reduce((list, codepoint) => { + if (codepoint.indexOf('-') === -1) { + list.push(String.fromCodePoint(`0x${codepoint}`)); + } else { + const hex_base = 16; + const [start, end] = codepoint.split('-').map((code) => parseInt(code, hex_base)); + for (let code = start; code <= end; code++) { + list.push(String.fromCodePoint(`0x${code.toString(hex_base)}`)); + } + } + return list; + }, []); + +const EMOJI_UNICODE_RANGE_REGEXP = new RegExp(`[${EMOJI_UNICODE_RANGES.join('')}]`, 'g'); +const WHITESPACES_REGEXP = /[\s\u200B-\u200D\uFEFF\uFE0E\uFE0F]/g; // also includes zero-width joiners + z.util.emoji = { includes_only_emojies: function(text) { - // http://www.unicode.org/Public/emoji/1.0/emoji-data.txt - // http://crocodillon.com/blog/parsing-emoji-unicode-in-javascript - const emoji_regex = /\ud83c[\udf00-\udfff]|\ud83c[\udde6-\uddff]|\ud83d[\udc00-\udeff]|\ud83e[\udd10-\uddff]|[\u231a-\u27ff][\ufe0f]?/g; - const is_valid_string = (string) => _.isString(string) && (string.length > 0); - const remove_emojies = (string) => string.replace(emoji_regex, ''); - const remove_whitespace = (string) => string.replace(/\s/g, ''); + const remove_emojies = (string) => string.replace(EMOJI_UNICODE_RANGE_REGEXP, ''); + const remove_whitespace = (string) => string.replace(WHITESPACES_REGEXP, ''); return is_valid_string(text) && (remove_emojies(remove_whitespace(text)).length === 0); }, diff --git a/app/script/util/Environment.js b/app/script/util/Environment.js index ef06edfb48e..e1660de5e5b 100644 --- a/app/script/util/Environment.js +++ b/app/script/util/Environment.js @@ -36,6 +36,7 @@ window.z.util = z.util || {}; ELECTRON: 'Electron', FIREFOX: 'Firefox', OPERA: 'Opera', + WIRE: 'Wire', }; const PLATFORM_NAME = { @@ -52,6 +53,9 @@ window.z.util = z.util || {}; is_chrome: function() { return platform.name === BROWSER_NAME.CHROME; }, + is_desktop: function() { + return this.is_electron() && navigator.userAgent.includes(BROWSER_NAME.WIRE); + }, is_edge: function() { return platform.name === BROWSER_NAME.EDGE; }, @@ -162,6 +166,7 @@ window.z.util = z.util || {}; }, version: _check.get_version(), }, + desktop: _check.is_desktop(), electron: _check.is_electron(), frontend: { is_localhost() { diff --git a/app/script/view_model/AuthViewModel.js b/app/script/view_model/AuthViewModel.js index 211f6d4ebe3..59c993232b8 100644 --- a/app/script/view_model/AuthViewModel.js +++ b/app/script/view_model/AuthViewModel.js @@ -353,20 +353,25 @@ z.ViewModel.AuthViewModel = class AuthViewModel { * @returns {Promise} Resolves when page is the first tab */ _check_single_instance(set_check_interval = true) { - if (Cookies.get(z.main.App.CONFIG.COOKIE_NAME)) { - this._handle_blocked_tabs(set_check_interval); - return Promise.reject(new z.auth.AuthError(z.auth.AuthError.TYPE.MULTIPLE_TABS)); - } + if (!z.util.Environment.electron) { + if (Cookies.get(z.main.App.CONFIG.TABS_CHECK.COOKIE_NAME)) { + this._handle_blocked_tabs(set_check_interval); + return Promise.reject(new z.auth.AuthError(z.auth.AuthError.TYPE.MULTIPLE_TABS)); + } - if (set_check_interval) { - this._set_tabs_check_interval(); + if (set_check_interval) { + this._set_tabs_check_interval(); + } } + return Promise.resolve(); } _clear_tabs_check_interval() { - window.clearInterval(this.tabs_check_interval_id); - this.tabs_check_interval_id = undefined; + if (this.tabs_check_interval_id) { + window.clearInterval(this.tabs_check_interval_id); + this.tabs_check_interval_id = undefined; + } } _handle_blocked_tabs(set_check_interval) { @@ -397,6 +402,7 @@ z.ViewModel.AuthViewModel = class AuthViewModel { this._handle_blocked_tabs(); }); }, 500); + $(window).on('unload', () => this._clear_tabs_check_interval()); } _set_tabs_recheck_interval() { @@ -527,6 +533,9 @@ z.ViewModel.AuthViewModel = class AuthViewModel { case z.service.BackendClientError.LABEL.PENDING_LOGIN: _on_code_request_success(error); break; + case z.service.BackendClientError.LABEL.PHONE_BUDGET_EXHAUSTED: + this._add_error(z.string.auth_error_phone_number_budget, z.auth.AuthView.TYPE.PHONE); + break; case z.service.BackendClientError.LABEL.UNAUTHORIZED: this._add_error(z.string.auth_error_phone_number_forbidden, z.auth.AuthView.TYPE.PHONE); break; diff --git a/app/script/view_model/ConversationInputViewModel.js b/app/script/view_model/ConversationInputViewModel.js index df95ff85d0c..b9bbf3c086f 100644 --- a/app/script/view_model/ConversationInputViewModel.js +++ b/app/script/view_model/ConversationInputViewModel.js @@ -162,7 +162,7 @@ z.ViewModel.ConversationInputViewModel = class ConversationInputViewModel { } ping() { - if (!this.ping_disabled()) { + if (this.conversation_et() && !this.ping_disabled()) { this.ping_disabled(true); this.conversation_repository.send_knock(this.conversation_et()) .then(() => { @@ -207,9 +207,14 @@ z.ViewModel.ConversationInputViewModel = class ConversationInputViewModel { this.logger.info(`Ephemeral timer for conversation '${this.conversation_et().display_name()}' is now at '${this.conversation_et().ephemeral_timer()}'.`); } + /** + * Post images to a conversation. + * @param {Array|FileList} images - Images + * @returns {undefined} No return value + */ upload_images(images) { if (!this._is_hitting_upload_limit(images)) { - for (const image of images) { + for (const image of [...images]) { if (image.size > z.config.MAXIMUM_IMAGE_FILE_SIZE) { return this._show_upload_warning(image); } @@ -219,9 +224,14 @@ z.ViewModel.ConversationInputViewModel = class ConversationInputViewModel { } } + /** + * Post files to a conversation. + * @param {Array|FileList} files - Images + * @returns {undefined} No return value + */ upload_files(files) { if (!this._is_hitting_upload_limit(files)) { - for (const file of files) { + for (const file of [...files]) { if (file.size > z.config.MAXIMUM_ASSET_FILE_SIZE) { amplify.publish(z.event.WebApp.ANALYTICS.EVENT, z.tracking.EventName.FILE.UPLOAD_TOO_BIG, {size: file.size, type: file.type}); amplify.publish(z.event.WebApp.AUDIO.PLAY, z.audio.AudioType.ALERT); diff --git a/app/script/view_model/ParticipantsViewModel.js b/app/script/view_model/ParticipantsViewModel.js index 3e008d049a3..e7779f4ae34 100644 --- a/app/script/view_model/ParticipantsViewModel.js +++ b/app/script/view_model/ParticipantsViewModel.js @@ -117,6 +117,13 @@ z.ViewModel.ParticipantsViewModel = class ParticipantsViewModel { }); // @todo create a viewmodel search? + this.search_action = ko.pureComputed(() => { + if (this.conversation()) { + const is_group = this.conversation().is_group() || this.conversation().is_team_group(); + return is_group ? z.string.people_confirm_label : z.string.search_open_group; + } + }); + this.user_input = ko.observable(''); this.user_selected = ko.observableArray([]); this.connected_users = ko.pureComputed(() => { diff --git a/app/script/view_model/VideoCallingViewModel.js b/app/script/view_model/VideoCallingViewModel.js index fbb7c940a1e..1dc35aca59a 100644 --- a/app/script/view_model/VideoCallingViewModel.js +++ b/app/script/view_model/VideoCallingViewModel.js @@ -209,7 +209,7 @@ z.ViewModel.VideoCallingViewModel = class VideoCallingViewModel { if (!this.disable_toggle_screen()) { if (this.self_stream_state.screen_send() || z.util.Environment.browser.firefox) { amplify.publish(z.event.WebApp.CALL.MEDIA.TOGGLE, conversation_id, z.media.MediaType.SCREEN); - } else if (z.util.Environment.electron) { + } else if (z.util.Environment.desktop) { amplify.publish(z.event.WebApp.ANALYTICS.EVENT, z.tracking.EventName.CALLING.SHARED_SCREEN, { conversation_type: this.joined_call().is_group ? z.tracking.attribute.ConversationType.GROUP : z.tracking.attribute.ConversationType.ONE_TO_ONE, kind_of_call_when_sharing: this.joined_call().is_remote_video_send() ? 'video' : 'audio', diff --git a/app/script/view_model/bindings/ConversationListBindings.js b/app/script/view_model/bindings/ConversationListBindings.js index 467b428a77e..6cf2601dc0d 100644 --- a/app/script/view_model/bindings/ConversationListBindings.js +++ b/app/script/view_model/bindings/ConversationListBindings.js @@ -24,13 +24,13 @@ ko.bindingHandlers.bordered_list = (function() { const calculate_borders = _.throttle(function($element) { if ($element) { window.requestAnimationFrame(function() { - const archive_column = $($element).parent(); + const list_column = $($element).parent(); if (($element.height() <= 0) || !$element.is_scrollable()) { - return archive_column.removeClass('left-list-center-border-bottom conversations-center-border-top'); + return list_column.removeClass('left-list-center-border-bottom conversations-center-border-top'); } - archive_column.toggleClass('left-list-center-border-top', !$element.is_scrolled_top()); - archive_column.toggleClass('left-list-center-border-bottom', !$element.is_scrolled_bottom()); + list_column.toggleClass('left-list-center-border-top', !$element.is_scrolled_top()); + list_column.toggleClass('left-list-center-border-bottom', !$element.is_scrolled_bottom()); }); } } diff --git a/app/script/view_model/list/StartUIViewModel.js b/app/script/view_model/list/StartUIViewModel.js index 41d837af402..03f5ef30254 100644 --- a/app/script/view_model/list/StartUIViewModel.js +++ b/app/script/view_model/list/StartUIViewModel.js @@ -227,10 +227,6 @@ z.ViewModel.list.StartUIViewModel = class StartUIViewModel { return z.l10n.text(z.string.invite_hint_unselected, meta_key); }); - this.invite_button_text = ko.pureComputed(() => { - return z.l10n.text(this.show_invite_form_only() ? z.string.people_invite : z.string.people_bring_your_friends); - }); - // Last open bubble this.user_bubble = undefined; this.user_bubble_last_id = undefined; @@ -675,16 +671,31 @@ z.ViewModel.list.StartUIViewModel = class StartUIViewModel { } _handle_search_input() { - const [matching_connection] = this.search_results.contacts(); const [matching_group] = this.search_results.groups(); - const [matching_team_member] = this.search_results.team_members(); - if (matching_connection && this.is_personal_space()) { + let matching_connection = undefined; + let matching_team_member = undefined; + + for (const user_et of this.search_results.contacts()) { + if (!this.selected_people().includes(user_et)) { + matching_connection = user_et; + break; + } + } + + for (const user_et of this.search_results.team_members()) { + if (!this.selected_people().includes(user_et)) { + matching_team_member = user_et; + break; + } + } + + if (this.is_personal_space() && matching_connection) { this.selected_people.push(matching_connection); return true; } - if (matching_team_member && !this.is_personal_space()) { + if (!this.is_personal_space() && matching_team_member) { this.selected_people.push(matching_team_member); return true; } diff --git a/app/style/common/variables.less b/app/style/common/variables.less index 76673887b04..600c1688eec 100644 --- a/app/style/common/variables.less +++ b/app/style/common/variables.less @@ -22,7 +22,23 @@ // ---------------------------------------------------------------------------- @font-face { font-family: emoji; - src: local('Apple Color Emoji'); + src: local('Apple Color Emoji'), local('Segoe UI Emoji'), local('Segoe UI Symbol'), local('Android Emoji'), local('Noto Color Emoji'), local('Emoji One'), local('Twemoji'); + + // Define a whitelist of glyphs to render with emoji font according to standard. + // http://unicode.org/Public/emoji/5.0/emoji-data.txt + // + // This list is constructed as following: + // 1. Download the list using the link above. + // 2. Remove comments and everything after semicolor on every line. + // 3. Sort and remove duplicate lines. + // 4. Remove the following lines (they are better off rendered with a text font): + // - 0023 (#) + // - 002A (*) + // - 0030..0039 (0-9) + // 5. Replace all occurrences of `..` with `-`. + // 6. Prepend each line with `U+`. + // 7. Join all lines with comma. + unicode-range: U+1F004, U+1F0CF, U+1F170-1F171, U+1F17E, U+1F17F, U+1F18E, U+1F191-1F19A, U+1F1E6-1F1FF, U+1F201, U+1F201-1F202, U+1F21A, U+1F22F, U+1F232-1F236, U+1F232-1F23A, U+1F238-1F23A, U+1F250-1F251, U+1F300-1F320, U+1F321, U+1F324-1F32C, U+1F32D-1F32F, U+1F330-1F335, U+1F336, U+1F337-1F37C, U+1F37D, U+1F37E-1F37F, U+1F380-1F393, U+1F385, U+1F396-1F397, U+1F399-1F39B, U+1F39E-1F39F, U+1F3A0-1F3C4, U+1F3C2-1F3C4, U+1F3C5, U+1F3C6-1F3CA, U+1F3C7, U+1F3CA, U+1F3CB-1F3CC, U+1F3CB-1F3CE, U+1F3CF-1F3D3, U+1F3D4-1F3DF, U+1F3E0-1F3F0, U+1F3F3-1F3F5, U+1F3F4, U+1F3F7, U+1F3F8-1F3FF, U+1F3FB-1F3FF, U+1F400-1F43E, U+1F43F, U+1F440, U+1F441, U+1F442-1F443, U+1F442-1F4F7, U+1F446-1F450, U+1F466-1F469, U+1F46E, U+1F470-1F478, U+1F47C, U+1F481-1F483, U+1F485-1F487, U+1F4AA, U+1F4F8, U+1F4F9-1F4FC, U+1F4FD, U+1F4FF, U+1F500-1F53D, U+1F549-1F54A, U+1F54B-1F54E, U+1F550-1F567, U+1F56F-1F570, U+1F573-1F579, U+1F574-1F575, U+1F57A, U+1F587, U+1F58A-1F58D, U+1F590, U+1F595-1F596, U+1F5A4, U+1F5A5, U+1F5A8, U+1F5B1-1F5B2, U+1F5BC, U+1F5C2-1F5C4, U+1F5D1-1F5D3, U+1F5DC-1F5DE, U+1F5E1, U+1F5E3, U+1F5E8, U+1F5EF, U+1F5F3, U+1F5FA, U+1F5FB-1F5FF, U+1F600, U+1F601-1F610, U+1F611, U+1F612-1F614, U+1F615, U+1F616, U+1F617, U+1F618, U+1F619, U+1F61A, U+1F61B, U+1F61C-1F61E, U+1F61F, U+1F620-1F625, U+1F626-1F627, U+1F628-1F62B, U+1F62C, U+1F62D, U+1F62E-1F62F, U+1F630-1F633, U+1F634, U+1F635-1F640, U+1F641-1F642, U+1F643-1F644, U+1F645-1F647, U+1F645-1F64F, U+1F64B-1F64F, U+1F680-1F6C5, U+1F6A3, U+1F6B4-1F6B6, U+1F6C0, U+1F6CB-1F6CF, U+1F6CC, U+1F6D0, U+1F6D1-1F6D2, U+1F6E0-1F6E5, U+1F6E9, U+1F6EB-1F6EC, U+1F6F0, U+1F6F3, U+1F6F4-1F6F6, U+1F6F7-1F6F8, U+1F910-1F918, U+1F918, U+1F919-1F91C, U+1F919-1F91E, U+1F91E, U+1F91F, U+1F920-1F927, U+1F926, U+1F928-1F92F, U+1F930, U+1F931-1F932, U+1F933-1F939, U+1F933-1F93A, U+1F93C-1F93E, U+1F93D-1F93E, U+1F940-1F945, U+1F947-1F94B, U+1F94C, U+1F950-1F95E, U+1F95F-1F96B, U+1F980-1F984, U+1F985-1F991, U+1F992-1F997, U+1F9C0, U+1F9D0-1F9E6, U+1F9D1-1F9DD, U+203C, U+2049, U+2122, U+2139, U+2194-2199, U+21A9-21AA, U+231A-231B, U+2328, U+23CF, U+23E9-23EC, U+23E9-23F3, U+23F0, U+23F3, U+23F8-23FA, U+24C2, U+25AA-25AB, U+25B6, U+25C0, U+25FB-25FE, U+25FD-25FE, U+2600-2604, U+260E, U+2611, U+2614-2615, U+2618, U+261D, U+2620, U+2622-2623, U+2626, U+262A, U+262E-262F, U+2638-263A, U+2640, U+2642, U+2648-2653, U+2660, U+2663, U+2665-2666, U+2668, U+267B, U+267F, U+2692-2697, U+2693, U+2699, U+269B-269C, U+26A0-26A1, U+26A1, U+26AA-26AB, U+26B0-26B1, U+26BD-26BE, U+26C4-26C5, U+26C8, U+26CE, U+26CF, U+26D1, U+26D3-26D4, U+26D4, U+26E9-26EA, U+26EA, U+26F0-26F5, U+26F2-26F3, U+26F5, U+26F7-26FA, U+26F9, U+26FA, U+26FD, U+2702, U+2705, U+2708-2709, U+270A-270B, U+270C-270D, U+270F, U+2712, U+2714, U+2716, U+271D, U+2721, U+2728, U+2733-2734, U+2744, U+2747, U+274C, U+274E, U+2753-2755, U+2757, U+2763-2764, U+2795-2797, U+27A1, U+27B0, U+27BF, U+2934-2935, U+2B05-2B07, U+2B1B-2B1C, U+2B50, U+2B55, U+3030, U+303D, U+3297, U+3299; } // ---------------------------------------------------------------------------- @@ -30,7 +46,7 @@ // ---------------------------------------------------------------------------- @ars-font-path: '../font/'; -@font-family-base: BlinkMacSystemFont, -apple-system, Helvetica Neue, Arial, sans-serif, emoji; +@font-family-base: emoji, BlinkMacSystemFont, -apple-system, Helvetica Neue, Arial, sans-serif; @font-family-ephemeral: Redacted Script; @font-size-xxs: 8px; diff --git a/app/style/components/user-input.less b/app/style/components/user-input.less index 997e4e26f6e..e7cdff60ea1 100644 --- a/app/style/components/user-input.less +++ b/app/style/components/user-input.less @@ -22,15 +22,17 @@ user-input { color: #fff; display: block; position: relative; + background: fade(#fff, 8%); + border-radius: 4px; &.user-list-light { - color: #222; + background: fade(@w-gray, 16%); + color: @graphite-dark; } } .search-outer { - padding-right: 16px; - padding-bottom: 12px; + padding: 4px 8px; overflow: hidden; } @@ -51,8 +53,6 @@ user-input { .label-bold-xs(); flex: 0 0 auto; line-height: @line-height-lg; - margin-bottom: 2px; - margin-top: 2px; padding-right: 16px; position: relative; @@ -83,36 +83,38 @@ user-input { flex: 1 1; height: @avatar-diameter-xs; line-height: @avatar-diameter-xs; - margin-right: 24px; min-width: 65px; + padding-left: -24px; outline: none; &:invalid { .accent-color(); + box-shadow: none; } &::placeholder { &:invalid { - color: fade(#fff, 48%); + color: fade(#fff, 40%); } .label-xs(); - color: fade(#fff, 48%); - padding-left: 8px; + color: fade(#fff, 40%); + text-align: center; + margin-left: -24px; } .user-list-light & { &::placeholder { - color: fade(#222, 48%); + color: fade(@graphite-dark, 40%); } } +} - &:invalid { - box-shadow: none; - } +.search-icon { + height: @avatar-diameter-xs; + line-height: @avatar-diameter-xs; + margin-right: 8px; } -.search-close { - position: absolute; - top: 6px; - right: 0; +.search-input-show-placeholder { + margin-right: 41px; } diff --git a/app/style/content/conversation/participants.less b/app/style/content/conversation/participants.less index 4a1bd0243d5..5b5ff0872c8 100644 --- a/app/style/content/conversation/participants.less +++ b/app/style/content/conversation/participants.less @@ -166,21 +166,46 @@ } .participants-search-header { - padding: 16px; + display: flex; + align-items: flex-start; + padding: 16px 8px 8px; +} + +.participants-search-header-list { + flex: 1 1 auto; +} + +.participants-search-header-close { + height: 32px; + margin-left: 7px; + margin-right: 8px; } -.participants-search-list-wrapper { +.participants-search-content { flex: 1 1 auto; position: relative; - margin-right: 16px; margin-left: 16px; } +.participants-search-footer { + align-items: center; + border-radius: 0; + display: flex; + flex: 0 0 auto; + line-height: 16px; + height: 48px; + padding: 16px; +} + +.participants-search-icon { + margin-right: 16px; +} .participants-search-list { .full-screen(); font-weight: 300; overflow-x: hidden; overflow-y: scroll; + padding-right: 16px; .everyone-participates { .text-center; diff --git a/app/style/fonts/zeta-neue.css b/app/style/fonts/zeta-neue.css index f329c2fcb70..010949fed2f 100755 --- a/app/style/fonts/zeta-neue.css +++ b/app/style/fonts/zeta-neue.css @@ -19,7 +19,7 @@ @font-face { font-family: 'Wire'; - src: url('/font/Wire.ttf?emoji') format('truetype'); + src: url('/font/Wire.ttf?invite') format('truetype'); font-weight: normal; font-style: normal; } @@ -237,6 +237,9 @@ .icon-end-call:before { content: "\e222"; } +.icon-invite:before { + content: "\e223"; +} .icon-sysmsg-error:before { content: "\e225"; } diff --git a/app/style/list/start-ui.less b/app/style/list/start-ui.less index 9be980cb78c..e0c49545b13 100644 --- a/app/style/list/start-ui.less +++ b/app/style/list/start-ui.less @@ -31,9 +31,7 @@ } .start-ui-header { - margin-left: 24px; - margin-right: 24px; - border-bottom: 1px solid fade(#fff, 16%) !important; + padding: 8px; } .start-ui-header-icons { @@ -61,8 +59,9 @@ } &.start-ui-header-actions-visible { + margin-top: 8px; transition-duration: @animation-timing-slow; - height: 44px; + height: 28px; } } @@ -129,18 +128,17 @@ } .start-ui-import { + .accent-background-color(); align-items: center; - border-top: 1px solid fade(#fff, 16%); + cursor: pointer; display: flex; flex: 0 0 auto; - justify-content: center; line-height: @conversation-input-min-height; height: @conversation-input-min-height; +} - > span { - cursor: pointer; - line-height: 32px; - } +.start-ui-import-icon { + margin: 0 16px; } .start-ui-user-bubble { diff --git a/bower.json b/bower.json index 563d9b235c7..f5a54b2bb6c 100644 --- a/bower.json +++ b/bower.json @@ -2,7 +2,7 @@ "name": "wire-webapp", "dependencies": { "amplify": "https://github.com/wireapp/amplify.git#1.1.4", - "antiscroll-2": "https://github.com/welovecoding/antiscroll-2.git#1.2.7", + "antiscroll-2": "https://github.com/wireapp/antiscroll-2.git#1.2.8", "cryptojs": "https://storage.googleapis.com/google-code-archive-downloads/v2/code.google.com/crypto-js/CryptoJS%20v3.1.2.zip", "dexie": "https://github.com/dfahlander/Dexie.js.git#1.5.1", "generic-message-proto": "https://github.com/wireapp/generic-message-proto.git#1.18.0", diff --git a/package.json b/package.json index 9227b38e55a..9b3d96bd6fc 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "devDependencies": { "autoprefixer": "7.1.1", "eslint": "3.19.0", - "eslint-plugin-jsdoc": "3.1.0", + "eslint-plugin-jsdoc": "3.1.1", "grunt": "1.0.1", "grunt-aws-s3": "1.0.0", "grunt-bower-task": "0.4.0", @@ -25,18 +25,18 @@ "grunt-postcss": "0.8.0", "grunt-shell": "2.1.0", "grunt-todo": "0.5.0", - "husky": "0.13.4", + "husky": "0.14.1", "jasmine-ajax": "3.2.0", - "jasmine-core": "2.5.2", + "jasmine-core": "2.6.4", "karma": "1.7.0", - "karma-chrome-launcher": "2.1.1", + "karma-chrome-launcher": "2.2.0", "karma-coverage": "1.1.1", "karma-jasmine": "1.1.0", "lint-staged": "4.0.0", "load-grunt-tasks": "3.5.2", "request": "2.81.0", "sinon": "2.3.5", - "stylelint": "7.11.0" + "stylelint": "7.12.0" }, "lint-staged": { "*.js": ["yarn lint-scripts", "git add"], diff --git a/test/unit_tests/cryptography/CryptographyRepositorySpec.js b/test/unit_tests/cryptography/CryptographyRepositorySpec.js index 6ea88f158ff..c89cbc0fb40 100644 --- a/test/unit_tests/cryptography/CryptographyRepositorySpec.js +++ b/test/unit_tests/cryptography/CryptographyRepositorySpec.js @@ -109,5 +109,20 @@ describe('z.cryptography.CryptographyRepository', function() { }) .catch(done.fail); }); + + it('only accept reasonable sized payload', function(done) { + // Length of this message is 1 320 024 while the maximum is 150% of 8 000 (12 000) + /* eslint-disable comma-spacing, key-spacing, sort-keys, quotes */ + const text = window.btoa(`https://wir${"\u0000\u0001\u0000\u000D\u0000A".repeat(165000)}e.com/`); + const event = {"conversation":"7bc4558b-18ce-446b-8e62-0c442b86ba56","time":"2017-06-15T22:18:55.071Z","data":{"text":text,"sender":"ccc17722a9348793","recipient":"4d7a36b30ef8bc26"},"from":"8549aada-07cc-4272-9fd4-c2ae040c539d","type":"conversation.otr-message-add"}; + /* eslint-enable comma-spacing, key-spacing, sort-keys, quotes */ + + TestFactory.cryptography_repository.handle_encrypted_event(event) + .then((mapped_event) => { + expect(mapped_event.type).toBe(z.event.Client.CONVERSATION.INCOMING_MESSAGE_TOO_BIG); + done(); + }) + .catch(done.fail); + }); }); }); diff --git a/test/unit_tests/util/EmojiSpec.js b/test/unit_tests/util/EmojiSpec.js index 0dedb8ca2c1..b3c7d7dc9e9 100644 --- a/test/unit_tests/util/EmojiSpec.js +++ b/test/unit_tests/util/EmojiSpec.js @@ -36,6 +36,7 @@ describe('z.util.emoji', function() { expect(z.util.emoji.includes_only_emojies('β›„')).toBeTruthy(); expect(z.util.emoji.includes_only_emojies('⚽')).toBeTruthy(); expect(z.util.emoji.includes_only_emojies('πŸ‡©πŸ‡°')).toBeTruthy(); + expect(z.util.emoji.includes_only_emojies('πŸŒοΈβ€β™€οΈ')).toBeTruthy(); }); it('returns true for text containing only emojies and whitespaces (Miscellaneous Symbols)', function() {