Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into export-conversations
This commit is contained in:
commit
45ce352d02
86 changed files with 2202 additions and 1969 deletions
748
src/@types/posthog.d.ts
vendored
748
src/@types/posthog.d.ts
vendored
|
@ -1,748 +0,0 @@
|
|||
// A clone of the type definitions from posthog-js, stripped of references to transitive
|
||||
// dependencies which we don't actually use, so that we don't need to install them.
|
||||
//
|
||||
// Original file lives in node_modules/posthog/dist/module.d.ts
|
||||
|
||||
/* eslint-disable @typescript-eslint/member-delimiter-style */
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
/* eslint-disable camelcase */
|
||||
|
||||
// Type definitions for exported methods
|
||||
|
||||
declare class posthog {
|
||||
/**
|
||||
* This function initializes a new instance of the PostHog capturing object.
|
||||
* All new instances are added to the main posthog object as sub properties (such as
|
||||
* posthog.library_name) and also returned by this function. To define a
|
||||
* second instance on the page, you would call:
|
||||
*
|
||||
* posthog.init('new token', { your: 'config' }, 'library_name');
|
||||
*
|
||||
* and use it like so:
|
||||
*
|
||||
* posthog.library_name.capture(...);
|
||||
*
|
||||
* @param {String} token Your PostHog API token
|
||||
* @param {Object} [config] A dictionary of config options to override. <a href="https://github.com/posthog/posthog-js/blob/6e0e873/src/posthog-core.js#L57-L91">See a list of default config options</a>.
|
||||
* @param {String} [name] The name for the new posthog instance that you want created
|
||||
*/
|
||||
static init(token: string, config?: posthog.Config, name?: string): posthog
|
||||
|
||||
/**
|
||||
* Clears super properties and generates a new random distinct_id for this instance.
|
||||
* Useful for clearing data when a user logs out.
|
||||
*/
|
||||
static reset(reset_device_id?: boolean): void
|
||||
|
||||
/**
|
||||
* Capture an event. This is the most important and
|
||||
* frequently used PostHog function.
|
||||
*
|
||||
* ### Usage:
|
||||
*
|
||||
* // capture an event named 'Registered'
|
||||
* posthog.capture('Registered', {'Gender': 'Male', 'Age': 21});
|
||||
*
|
||||
* // capture an event using navigator.sendBeacon
|
||||
* posthog.capture('Left page', {'duration_seconds': 35}, {transport: 'sendBeacon'});
|
||||
*
|
||||
* @param {String} event_name The name of the event. This can be anything the user does - 'Button Click', 'Sign Up', 'Item Purchased', etc.
|
||||
* @param {Object} [properties] A set of properties to include with the event you're sending. These describe the user who did the event or details about the event itself.
|
||||
* @param {Object} [options] Optional configuration for this capture request.
|
||||
* @param {String} [options.transport] Transport method for network request ('XHR' or 'sendBeacon').
|
||||
*/
|
||||
static capture(
|
||||
event_name: string,
|
||||
properties?: posthog.Properties,
|
||||
options?: { transport: 'XHR' | 'sendBeacon' }
|
||||
): posthog.CaptureResult
|
||||
|
||||
/**
|
||||
* Capture a page view event, which is currently ignored by the server.
|
||||
* This function is called by default on page load unless the
|
||||
* capture_pageview configuration variable is false.
|
||||
*
|
||||
* @param {String} [page] The url of the page to record. If you don't include this, it defaults to the current url.
|
||||
* @api private
|
||||
*/
|
||||
static capture_pageview(page?: string): void
|
||||
|
||||
/**
|
||||
* Register a set of super properties, which are included with all
|
||||
* events. This will overwrite previous super property values.
|
||||
*
|
||||
* ### Usage:
|
||||
*
|
||||
* // register 'Gender' as a super property
|
||||
* posthog.register({'Gender': 'Female'});
|
||||
*
|
||||
* // register several super properties when a user signs up
|
||||
* posthog.register({
|
||||
* 'Email': 'jdoe@example.com',
|
||||
* 'Account Type': 'Free'
|
||||
* });
|
||||
*
|
||||
* @param {Object} properties An associative array of properties to store about the user
|
||||
* @param {Number} [days] How many days since the user's last visit to store the super properties
|
||||
*/
|
||||
static register(properties: posthog.Properties, days?: number): void
|
||||
|
||||
/**
|
||||
* Register a set of super properties only once. This will not
|
||||
* overwrite previous super property values, unlike register().
|
||||
*
|
||||
* ### Usage:
|
||||
*
|
||||
* // register a super property for the first time only
|
||||
* posthog.register_once({
|
||||
* 'First Login Date': new Date().toISOString()
|
||||
* });
|
||||
*
|
||||
* ### Notes:
|
||||
*
|
||||
* If default_value is specified, current super properties
|
||||
* with that value will be overwritten.
|
||||
*
|
||||
* @param {Object} properties An associative array of properties to store about the user
|
||||
* @param {*} [default_value] Value to override if already set in super properties (ex: 'False') Default: 'None'
|
||||
* @param {Number} [days] How many days since the users last visit to store the super properties
|
||||
*/
|
||||
static register_once(properties: posthog.Properties, default_value?: posthog.Property, days?: number): void
|
||||
|
||||
/**
|
||||
* Delete a super property stored with the current user.
|
||||
*
|
||||
* @param {String} property The name of the super property to remove
|
||||
*/
|
||||
static unregister(property: string): void
|
||||
|
||||
/**
|
||||
* Identify a user with a unique ID instead of a PostHog
|
||||
* randomly generated distinct_id. If the method is never called,
|
||||
* then unique visitors will be identified by a UUID generated
|
||||
* the first time they visit the site.
|
||||
*
|
||||
* If user properties are passed, they are also sent to posthog.
|
||||
*
|
||||
* ### Usage:
|
||||
*
|
||||
* posthog.identify('[user unique id]')
|
||||
* posthog.identify('[user unique id]', { email: 'john@example.com' })
|
||||
* posthog.identify('[user unique id]', {}, { referral_code: '12345' })
|
||||
*
|
||||
* ### Notes:
|
||||
*
|
||||
* You can call this function to overwrite a previously set
|
||||
* unique ID for the current user. PostHog cannot translate
|
||||
* between IDs at this time, so when you change a user's ID
|
||||
* they will appear to be a new user.
|
||||
*
|
||||
* When used alone, posthog.identify will change the user's
|
||||
* distinct_id to the unique ID provided. When used in tandem
|
||||
* with posthog.alias, it will allow you to identify based on
|
||||
* unique ID and map that back to the original, anonymous
|
||||
* distinct_id given to the user upon her first arrival to your
|
||||
* site (thus connecting anonymous pre-signup activity to
|
||||
* post-signup activity). Though the two work together, do not
|
||||
* call identify() at the same time as alias(). Calling the two
|
||||
* at the same time can cause a race condition, so it is best
|
||||
* practice to call identify on the original, anonymous ID
|
||||
* right after you've aliased it.
|
||||
*
|
||||
* @param {String} [unique_id] A string that uniquely identifies a user. If not provided, the distinct_id currently in the persistent store (cookie or localStorage) will be used.
|
||||
* @param {Object} [userProperties] Optional: An associative array of properties to store about the user
|
||||
* @param {Object} [userPropertiesToSetOnce] Optional: An associative array of properties to store about the user. If property is previously set, this does not override that value.
|
||||
*/
|
||||
static identify(
|
||||
unique_id?: string,
|
||||
userPropertiesToSet?: posthog.Properties,
|
||||
userPropertiesToSetOnce?: posthog.Properties
|
||||
): void
|
||||
|
||||
/**
|
||||
* Create an alias, which PostHog will use to link two distinct_ids going forward (not retroactively).
|
||||
* Multiple aliases can map to the same original ID, but not vice-versa. Aliases can also be chained - the
|
||||
* following is a valid scenario:
|
||||
*
|
||||
* posthog.alias('new_id', 'existing_id');
|
||||
* ...
|
||||
* posthog.alias('newer_id', 'new_id');
|
||||
*
|
||||
* If the original ID is not passed in, we will use the current distinct_id - probably the auto-generated GUID.
|
||||
*
|
||||
* ### Notes:
|
||||
*
|
||||
* The best practice is to call alias() when a unique ID is first created for a user
|
||||
* (e.g., when a user first registers for an account and provides an email address).
|
||||
* alias() should never be called more than once for a given user, except to
|
||||
* chain a newer ID to a previously new ID, as described above.
|
||||
*
|
||||
* @param {String} alias A unique identifier that you want to use for this user in the future.
|
||||
* @param {String} [original] The current identifier being used for this user.
|
||||
*/
|
||||
static alias(alias: string, original?: string): posthog.CaptureResult | number
|
||||
|
||||
/**
|
||||
* Update the configuration of a posthog library instance.
|
||||
*
|
||||
* The default config is:
|
||||
*
|
||||
* {
|
||||
* // HTTP method for capturing requests
|
||||
* api_method: 'POST'
|
||||
*
|
||||
* // transport for sending requests ('XHR' or 'sendBeacon')
|
||||
* // NB: sendBeacon should only be used for scenarios such as
|
||||
* // page unload where a "best-effort" attempt to send is
|
||||
* // acceptable; the sendBeacon API does not support callbacks
|
||||
* // or any way to know the result of the request. PostHog
|
||||
* // capturing via sendBeacon will not support any event-
|
||||
* // batching or retry mechanisms.
|
||||
* api_transport: 'XHR'
|
||||
*
|
||||
* // Automatically capture clicks, form submissions and change events
|
||||
* autocapture: true
|
||||
*
|
||||
* // Capture rage clicks (beta) - useful for session recording
|
||||
* rageclick: false
|
||||
*
|
||||
* // super properties cookie expiration (in days)
|
||||
* cookie_expiration: 365
|
||||
*
|
||||
* // super properties span subdomains
|
||||
* cross_subdomain_cookie: true
|
||||
*
|
||||
* // debug mode
|
||||
* debug: false
|
||||
*
|
||||
* // if this is true, the posthog cookie or localStorage entry
|
||||
* // will be deleted, and no user persistence will take place
|
||||
* disable_persistence: false
|
||||
*
|
||||
* // if this is true, PostHog will automatically determine
|
||||
* // City, Region and Country data using the IP address of
|
||||
* //the client
|
||||
* ip: true
|
||||
*
|
||||
* // opt users out of capturing by this PostHog instance by default
|
||||
* opt_out_capturing_by_default: false
|
||||
*
|
||||
* // opt users out of browser data storage by this PostHog instance by default
|
||||
* opt_out_persistence_by_default: false
|
||||
*
|
||||
* // persistence mechanism used by opt-in/opt-out methods - cookie
|
||||
* // or localStorage - falls back to cookie if localStorage is unavailable
|
||||
* opt_out_capturing_persistence_type: 'localStorage'
|
||||
*
|
||||
* // customize the name of cookie/localStorage set by opt-in/opt-out methods
|
||||
* opt_out_capturing_cookie_prefix: null
|
||||
*
|
||||
* // type of persistent store for super properties (cookie/
|
||||
* // localStorage) if set to 'localStorage', any existing
|
||||
* // posthog cookie value with the same persistence_name
|
||||
* // will be transferred to localStorage and deleted
|
||||
* persistence: 'cookie'
|
||||
*
|
||||
* // name for super properties persistent store
|
||||
* persistence_name: ''
|
||||
*
|
||||
* // names of properties/superproperties which should never
|
||||
* // be sent with capture() calls
|
||||
* property_blacklist: []
|
||||
*
|
||||
* // if this is true, posthog cookies will be marked as
|
||||
* // secure, meaning they will only be transmitted over https
|
||||
* secure_cookie: false
|
||||
*
|
||||
* // should we capture a page view on page load
|
||||
* capture_pageview: true
|
||||
*
|
||||
* // if you set upgrade to be true, the library will check for
|
||||
* // a cookie from our old js library and import super
|
||||
* // properties from it, then the old cookie is deleted
|
||||
* // The upgrade config option only works in the initialization,
|
||||
* // so make sure you set it when you create the library.
|
||||
* upgrade: false
|
||||
*
|
||||
* // extra HTTP request headers to set for each API request, in
|
||||
* // the format {'Header-Name': value}
|
||||
* xhr_headers: {}
|
||||
*
|
||||
* // protocol for fetching in-app message resources, e.g.
|
||||
* // 'https://' or 'http://'; defaults to '//' (which defers to the
|
||||
* // current page's protocol)
|
||||
* inapp_protocol: '//'
|
||||
*
|
||||
* // whether to open in-app message link in new tab/window
|
||||
* inapp_link_new_window: false
|
||||
*
|
||||
* // a set of rrweb config options that PostHog users can configure
|
||||
* // see https://github.com/rrweb-io/rrweb/blob/master/guide.md
|
||||
* session_recording: {
|
||||
* blockClass: 'ph-no-capture',
|
||||
* blockSelector: null,
|
||||
* ignoreClass: 'ph-ignore-input',
|
||||
* maskAllInputs: false,
|
||||
* maskInputOptions: {},
|
||||
* maskInputFn: null,
|
||||
* slimDOMOptions: {},
|
||||
* collectFonts: false
|
||||
* }
|
||||
*
|
||||
* // prevent autocapture from capturing any attribute names on elements
|
||||
* mask_all_element_attributes: false
|
||||
*
|
||||
* // prevent autocapture from capturing textContent on all elements
|
||||
* mask_all_text: false
|
||||
*
|
||||
* // will disable requests to the /decide endpoint (please review documentation for details)
|
||||
* // autocapture, feature flags, compression and session recording will be disabled when set to `true`
|
||||
* advanced_disable_decide: false
|
||||
*
|
||||
* }
|
||||
*
|
||||
*
|
||||
* @param {Object} config A dictionary of new configuration values to update
|
||||
*/
|
||||
static set_config(config: posthog.Config): void
|
||||
|
||||
/**
|
||||
* returns the current config object for the library.
|
||||
*/
|
||||
static get_config<T extends keyof posthog.Config>(prop_name: T): posthog.Config[T]
|
||||
|
||||
/**
|
||||
* Returns the value of the super property named property_name. If no such
|
||||
* property is set, get_property() will return the undefined value.
|
||||
*
|
||||
* ### Notes:
|
||||
*
|
||||
* get_property() can only be called after the PostHog library has finished loading.
|
||||
* init() has a loaded function available to handle this automatically. For example:
|
||||
*
|
||||
* // grab value for 'user_id' after the posthog library has loaded
|
||||
* posthog.init('YOUR PROJECT TOKEN', {
|
||||
* loaded: function(posthog) {
|
||||
* user_id = posthog.get_property('user_id');
|
||||
* }
|
||||
* });
|
||||
*
|
||||
* @param {String} property_name The name of the super property you want to retrieve
|
||||
*/
|
||||
static get_property(property_name: string): posthog.Property | undefined
|
||||
|
||||
/**
|
||||
* Returns the current distinct id of the user. This is either the id automatically
|
||||
* generated by the library or the id that has been passed by a call to identify().
|
||||
*
|
||||
* ### Notes:
|
||||
*
|
||||
* get_distinct_id() can only be called after the PostHog library has finished loading.
|
||||
* init() has a loaded function available to handle this automatically. For example:
|
||||
*
|
||||
* // set distinct_id after the posthog library has loaded
|
||||
* posthog.init('YOUR PROJECT TOKEN', {
|
||||
* loaded: function(posthog) {
|
||||
* distinct_id = posthog.get_distinct_id();
|
||||
* }
|
||||
* });
|
||||
*/
|
||||
static get_distinct_id(): string
|
||||
|
||||
/**
|
||||
* Opt the user out of data capturing and cookies/localstorage for this PostHog instance
|
||||
*
|
||||
* ### Usage
|
||||
*
|
||||
* // opt user out
|
||||
* posthog.opt_out_capturing();
|
||||
*
|
||||
* // opt user out with different cookie configuration from PostHog instance
|
||||
* posthog.opt_out_capturing({
|
||||
* cookie_expiration: 30,
|
||||
* secure_cookie: true
|
||||
* });
|
||||
*
|
||||
* @param {Object} [options] A dictionary of config options to override
|
||||
* @param {boolean} [options.clear_persistence=true] If true, will delete all data stored by the sdk in persistence
|
||||
* @param {string} [options.persistence_type=localStorage] Persistence mechanism used - cookie or localStorage - falls back to cookie if localStorage is unavailable
|
||||
* @param {string} [options.cookie_prefix=__ph_opt_in_out] Custom prefix to be used in the cookie/localstorage name
|
||||
* @param {Number} [options.cookie_expiration] Number of days until the opt-in cookie expires (overrides value specified in this PostHog instance's config)
|
||||
* @param {boolean} [options.cross_subdomain_cookie] Whether the opt-in cookie is set as cross-subdomain or not (overrides value specified in this PostHog instance's config)
|
||||
* @param {boolean} [options.secure_cookie] Whether the opt-in cookie is set as secure or not (overrides value specified in this PostHog instance's config)
|
||||
*/
|
||||
static opt_out_capturing(options?: posthog.OptInOutCapturingOptions): void
|
||||
|
||||
/**
|
||||
* Opt the user in to data capturing and cookies/localstorage for this PostHog instance
|
||||
*
|
||||
* ### Usage
|
||||
*
|
||||
* // opt user in
|
||||
* posthog.opt_in_capturing();
|
||||
*
|
||||
* // opt user in with specific event name, properties, cookie configuration
|
||||
* posthog.opt_in_capturing({
|
||||
* capture_event_name: 'User opted in',
|
||||
* capture_event_properties: {
|
||||
* 'Email': 'jdoe@example.com'
|
||||
* },
|
||||
* cookie_expiration: 30,
|
||||
* secure_cookie: true
|
||||
* });
|
||||
*
|
||||
* @param {Object} [options] A dictionary of config options to override
|
||||
* @param {function} [options.capture] Function used for capturing a PostHog event to record the opt-in action (default is this PostHog instance's capture method)
|
||||
* @param {string} [options.capture_event_name=$opt_in] Event name to be used for capturing the opt-in action
|
||||
* @param {Object} [options.capture_properties] Set of properties to be captured along with the opt-in action
|
||||
* @param {boolean} [options.enable_persistence=true] If true, will re-enable sdk persistence
|
||||
* @param {string} [options.persistence_type=localStorage] Persistence mechanism used - cookie or localStorage - falls back to cookie if localStorage is unavailable
|
||||
* @param {string} [options.cookie_prefix=__ph_opt_in_out] Custom prefix to be used in the cookie/localstorage name
|
||||
* @param {Number} [options.cookie_expiration] Number of days until the opt-in cookie expires (overrides value specified in this PostHog instance's config)
|
||||
* @param {boolean} [options.cross_subdomain_cookie] Whether the opt-in cookie is set as cross-subdomain or not (overrides value specified in this PostHog instance's config)
|
||||
* @param {boolean} [options.secure_cookie] Whether the opt-in cookie is set as secure or not (overrides value specified in this PostHog instance's config)
|
||||
*/
|
||||
static opt_in_capturing(options?: posthog.OptInOutCapturingOptions): void
|
||||
|
||||
/**
|
||||
* Check whether the user has opted out of data capturing and cookies/localstorage for this PostHog instance
|
||||
*
|
||||
* ### Usage
|
||||
*
|
||||
* const has_opted_out = posthog.has_opted_out_capturing();
|
||||
* // use has_opted_out value
|
||||
*
|
||||
* @param {Object} [options] A dictionary of config options to override
|
||||
* @param {string} [options.persistence_type=localStorage] Persistence mechanism used - cookie or localStorage - falls back to cookie if localStorage is unavailable
|
||||
* @param {string} [options.cookie_prefix=__ph_opt_in_out] Custom prefix to be used in the cookie/localstorage name
|
||||
* @returns {boolean} current opt-out status
|
||||
*/
|
||||
static has_opted_out_capturing(options?: posthog.HasOptedInOutCapturingOptions): boolean
|
||||
|
||||
/**
|
||||
* Check whether the user has opted in to data capturing and cookies/localstorage for this PostHog instance
|
||||
*
|
||||
* ### Usage
|
||||
*
|
||||
* const has_opted_in = posthog.has_opted_in_capturing();
|
||||
* // use has_opted_in value
|
||||
*
|
||||
* @param {Object} [options] A dictionary of config options to override
|
||||
* @param {string} [options.persistence_type=localStorage] Persistence mechanism used - cookie or localStorage - falls back to cookie if localStorage is unavailable
|
||||
* @param {string} [options.cookie_prefix=__ph_opt_in_out] Custom prefix to be used in the cookie/localstorage name
|
||||
* @returns {boolean} current opt-in status
|
||||
*/
|
||||
static has_opted_in_capturing(options?: posthog.HasOptedInOutCapturingOptions): boolean
|
||||
|
||||
/**
|
||||
* Clear the user's opt in/out status of data capturing and cookies/localstorage for this PostHog instance
|
||||
*
|
||||
* ### Usage
|
||||
*
|
||||
* // clear user's opt-in/out status
|
||||
* posthog.clear_opt_in_out_capturing();
|
||||
*
|
||||
* // clear user's opt-in/out status with specific cookie configuration - should match
|
||||
* // configuration used when opt_in_capturing/opt_out_capturing methods were called.
|
||||
* posthog.clear_opt_in_out_capturing({
|
||||
* cookie_expiration: 30,
|
||||
* secure_cookie: true
|
||||
* });
|
||||
*
|
||||
* @param {Object} [options] A dictionary of config options to override
|
||||
* @param {boolean} [options.enable_persistence=true] If true, will re-enable sdk persistence
|
||||
* @param {string} [options.persistence_type=localStorage] Persistence mechanism used - cookie or localStorage - falls back to cookie if localStorage is unavailable
|
||||
* @param {string} [options.cookie_prefix=__ph_opt_in_out] Custom prefix to be used in the cookie/localstorage name
|
||||
* @param {Number} [options.cookie_expiration] Number of days until the opt-in cookie expires (overrides value specified in this PostHog instance's config)
|
||||
* @param {boolean} [options.cross_subdomain_cookie] Whether the opt-in cookie is set as cross-subdomain or not (overrides value specified in this PostHog instance's config)
|
||||
* @param {boolean} [options.secure_cookie] Whether the opt-in cookie is set as secure or not (overrides value specified in this PostHog instance's config)
|
||||
*/
|
||||
static clear_opt_in_out_capturing(options?: posthog.ClearOptInOutCapturingOptions): void
|
||||
|
||||
/*
|
||||
* See if feature flag is enabled for user.
|
||||
*
|
||||
* ### Usage:
|
||||
*
|
||||
* if(posthog.isFeatureEnabled('beta-feature')) { // do something }
|
||||
*
|
||||
* @param {Object|String} prop Key of the feature flag.
|
||||
* @param {Object|String} options (optional) If {send_event: false}, we won't send an $feature_flag_call event to PostHog.
|
||||
*/
|
||||
static isFeatureEnabled(key: string, options?: posthog.isFeatureEnabledOptions): boolean
|
||||
|
||||
/*
|
||||
* See if feature flags are available.
|
||||
*
|
||||
* ### Usage:
|
||||
*
|
||||
* posthog.onFeatureFlags(function(featureFlags) { // do something })
|
||||
*
|
||||
* @param {Function} [callback] The callback function will be called once the feature flags are ready. It'll return a list of feature flags enabled for the user.
|
||||
*/
|
||||
static onFeatureFlags(callback: (flags: string[]) => void): false | undefined
|
||||
|
||||
/*
|
||||
* Reload all feature flags for the user.
|
||||
*
|
||||
* ### Usage:
|
||||
*
|
||||
* posthog.reloadFeatureFlags()
|
||||
*/
|
||||
static reloadFeatureFlags(): void
|
||||
|
||||
static toString(): string
|
||||
|
||||
/* Will log all capture requests to the Javascript console, including event properties for easy debugging */
|
||||
static debug(): void
|
||||
|
||||
/*
|
||||
* Starts session recording and updates disable_session_recording to false.
|
||||
* Used for manual session recording management. By default, session recording is enabled and
|
||||
* starts automatically.
|
||||
*
|
||||
* ### Usage:
|
||||
*
|
||||
* posthog.startSessionRecording()
|
||||
*/
|
||||
static startSessionRecording(): void
|
||||
|
||||
/*
|
||||
* Stops session recording and updates disable_session_recording to true.
|
||||
*
|
||||
* ### Usage:
|
||||
*
|
||||
* posthog.stopSessionRecording()
|
||||
*/
|
||||
static stopSessionRecording(): void
|
||||
|
||||
/*
|
||||
* Check if session recording is currently running.
|
||||
*
|
||||
* ### Usage:
|
||||
*
|
||||
* const isSessionRecordingOn = posthog.sessionRecordingStarted()
|
||||
*/
|
||||
static sessionRecordingStarted(): boolean
|
||||
}
|
||||
|
||||
declare namespace posthog {
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
type Property = any;
|
||||
type Properties = Record<string, Property>;
|
||||
type CaptureResult = { event: string; properties: Properties } | undefined;
|
||||
type CaptureCallback = (response: any, data: any) => void;
|
||||
/* eslint-enable @typescript-eslint/no-explicit-any */
|
||||
|
||||
interface Config {
|
||||
api_host?: string
|
||||
api_method?: string
|
||||
api_transport?: string
|
||||
autocapture?: boolean
|
||||
rageclick?: boolean
|
||||
cdn?: string
|
||||
cross_subdomain_cookie?: boolean
|
||||
persistence?: 'localStorage' | 'cookie' | 'memory'
|
||||
persistence_name?: string
|
||||
cookie_name?: string
|
||||
loaded?: (posthog_instance: typeof posthog) => void
|
||||
store_google?: boolean
|
||||
save_referrer?: boolean
|
||||
test?: boolean
|
||||
verbose?: boolean
|
||||
img?: boolean
|
||||
capture_pageview?: boolean
|
||||
debug?: boolean
|
||||
cookie_expiration?: number
|
||||
upgrade?: boolean
|
||||
disable_session_recording?: boolean
|
||||
disable_persistence?: boolean
|
||||
disable_cookie?: boolean
|
||||
secure_cookie?: boolean
|
||||
ip?: boolean
|
||||
opt_out_capturing_by_default?: boolean
|
||||
opt_out_persistence_by_default?: boolean
|
||||
opt_out_capturing_persistence_type?: 'localStorage' | 'cookie'
|
||||
opt_out_capturing_cookie_prefix?: string | null
|
||||
respect_dnt?: boolean
|
||||
property_blacklist?: string[]
|
||||
xhr_headers?: { [header_name: string]: string }
|
||||
inapp_protocol?: string
|
||||
inapp_link_new_window?: boolean
|
||||
request_batching?: boolean
|
||||
sanitize_properties?: (properties: posthog.Properties, event_name: string) => posthog.Properties
|
||||
properties_string_max_length?: number
|
||||
mask_all_element_attributes?: boolean
|
||||
mask_all_text?: boolean
|
||||
advanced_disable_decide?: boolean
|
||||
}
|
||||
|
||||
interface OptInOutCapturingOptions {
|
||||
clear_persistence: boolean
|
||||
persistence_type: string
|
||||
cookie_prefix: string
|
||||
cookie_expiration: number
|
||||
cross_subdomain_cookie: boolean
|
||||
secure_cookie: boolean
|
||||
}
|
||||
|
||||
interface HasOptedInOutCapturingOptions {
|
||||
persistence_type: string
|
||||
cookie_prefix: string
|
||||
}
|
||||
|
||||
interface ClearOptInOutCapturingOptions {
|
||||
enable_persistence: boolean
|
||||
persistence_type: string
|
||||
cookie_prefix: string
|
||||
cookie_expiration: number
|
||||
cross_subdomain_cookie: boolean
|
||||
secure_cookie: boolean
|
||||
}
|
||||
|
||||
interface isFeatureEnabledOptions {
|
||||
send_event: boolean
|
||||
}
|
||||
|
||||
export class persistence {
|
||||
static properties(): posthog.Properties
|
||||
|
||||
static load(): void
|
||||
|
||||
static save(): void
|
||||
|
||||
static remove(): void
|
||||
|
||||
static clear(): void
|
||||
|
||||
/**
|
||||
* @param {Object} props
|
||||
* @param {*=} default_value
|
||||
* @param {number=} days
|
||||
*/
|
||||
static register_once(props: Properties, default_value?: Property, days?: number): boolean
|
||||
|
||||
/**
|
||||
* @param {Object} props
|
||||
* @param {number=} days
|
||||
*/
|
||||
static register(props: posthog.Properties, days?: number): boolean
|
||||
|
||||
static unregister(prop: string): void
|
||||
|
||||
static update_campaign_params(): void
|
||||
|
||||
static update_search_keyword(referrer: string): void
|
||||
|
||||
static update_referrer_info(referrer: string): void
|
||||
|
||||
static get_referrer_info(): posthog.Properties
|
||||
|
||||
static safe_merge(props: posthog.Properties): posthog.Properties
|
||||
|
||||
static update_config(config: posthog.Config): void
|
||||
|
||||
static set_disabled(disabled: boolean): void
|
||||
|
||||
static set_cross_subdomain(cross_subdomain: boolean): void
|
||||
|
||||
static get_cross_subdomain(): boolean
|
||||
|
||||
static set_secure(secure: boolean): void
|
||||
|
||||
static set_event_timer(event_name: string, timestamp: Date): void
|
||||
|
||||
static remove_event_timer(event_name: string): Date | undefined
|
||||
}
|
||||
|
||||
export class people {
|
||||
/*
|
||||
* Set properties on a user record.
|
||||
*
|
||||
* ### Usage:
|
||||
*
|
||||
* posthog.people.set('gender', 'm');
|
||||
*
|
||||
* // or set multiple properties at once
|
||||
* posthog.people.set({
|
||||
* 'Company': 'Acme',
|
||||
* 'Plan': 'Premium',
|
||||
* 'Upgrade date': new Date()
|
||||
* });
|
||||
* // properties can be strings, integers, dates, or lists
|
||||
*
|
||||
* @param {Object|String} prop If a string, this is the name of the property. If an object, this is an associative array of names and values.
|
||||
* @param {*} [to] A value to set on the given property name
|
||||
* @param {Function} [callback] If provided, the callback will be called after capturing the event.
|
||||
*/
|
||||
static set(
|
||||
prop: posthog.Properties | string,
|
||||
to?: posthog.Property,
|
||||
callback?: posthog.CaptureCallback
|
||||
): posthog.Properties
|
||||
|
||||
/*
|
||||
* Set properties on a user record, only if they do not yet exist.
|
||||
* This will not overwrite previous people property values, unlike
|
||||
* people.set().
|
||||
*
|
||||
* ### Usage:
|
||||
*
|
||||
* posthog.people.set_once('First Login Date', new Date());
|
||||
*
|
||||
* // or set multiple properties at once
|
||||
* posthog.people.set_once({
|
||||
* 'First Login Date': new Date(),
|
||||
* 'Starting Plan': 'Premium'
|
||||
* });
|
||||
*
|
||||
* // properties can be strings, integers or dates
|
||||
*
|
||||
* @param {Object|String} prop If a string, this is the name of the property. If an object, this is an associative array of names and values.
|
||||
* @param {*} [to] A value to set on the given property name
|
||||
* @param {Function} [callback] If provided, the callback will be called after capturing the event.
|
||||
*/
|
||||
static set_once(
|
||||
prop: posthog.Properties | string,
|
||||
to?: posthog.Property,
|
||||
callback?: posthog.CaptureCallback
|
||||
): posthog.Properties
|
||||
|
||||
static toString(): string
|
||||
}
|
||||
|
||||
export class featureFlags {
|
||||
static getFlags(): string[]
|
||||
|
||||
static reloadFeatureFlags(): void
|
||||
|
||||
/*
|
||||
* See if feature flag is enabled for user.
|
||||
*
|
||||
* ### Usage:
|
||||
*
|
||||
* if(posthog.isFeatureEnabled('beta-feature')) { // do something }
|
||||
*
|
||||
* @param {Object|String} prop Key of the feature flag.
|
||||
* @param {Object|String} options (optional) If {send_event: false}, we won't send an $feature_flag_call event to PostHog.
|
||||
*/
|
||||
static isFeatureEnabled(key: string, options?: { send_event?: boolean }): boolean
|
||||
|
||||
/*
|
||||
* See if feature flags are available.
|
||||
*
|
||||
* ### Usage:
|
||||
*
|
||||
* posthog.onFeatureFlags(function(featureFlags) { // do something })
|
||||
*
|
||||
* @param {Function} [callback] The callback function will be called once the feature flags are ready. It'll return a list of feature flags enabled for the user.
|
||||
*/
|
||||
static onFeatureFlags(callback: (flags: string[]) => void): false | undefined
|
||||
}
|
||||
|
||||
export class feature_flags extends featureFlags {}
|
||||
}
|
||||
|
||||
export type PostHog = typeof posthog;
|
||||
|
||||
export default posthog;
|
|
@ -1,7 +1,8 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017, 2018 New Vector Ltd
|
||||
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2019 - 2021 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -60,7 +61,6 @@ import Modal from './Modal';
|
|||
import { _t } from './languageHandler';
|
||||
import dis from './dispatcher/dispatcher';
|
||||
import WidgetUtils from './utils/WidgetUtils';
|
||||
import WidgetEchoStore from './stores/WidgetEchoStore';
|
||||
import SettingsStore from './settings/SettingsStore';
|
||||
import { Jitsi } from "./widgets/Jitsi";
|
||||
import { WidgetType } from "./widgets/WidgetType";
|
||||
|
@ -86,6 +86,12 @@ import { randomUppercaseString, randomLowercaseString } from "matrix-js-sdk/src/
|
|||
import EventEmitter from 'events';
|
||||
import SdkConfig from './SdkConfig';
|
||||
import { ensureDMExists, findDMForUser } from './createRoom';
|
||||
import { IPushRule, RuleId, TweakName, Tweaks } from "matrix-js-sdk/src/@types/PushRules";
|
||||
import { PushProcessor } from 'matrix-js-sdk/src/pushprocessor';
|
||||
import { WidgetLayoutStore, Container } from './stores/widgets/WidgetLayoutStore';
|
||||
import { getIncomingCallToastKey } from './toasts/IncomingCallToast';
|
||||
import ToastStore from './stores/ToastStore';
|
||||
import IncomingCallToast from "./toasts/IncomingCallToast";
|
||||
|
||||
export const PROTOCOL_PSTN = 'm.protocol.pstn';
|
||||
export const PROTOCOL_PSTN_PREFIXED = 'im.vector.protocol.pstn';
|
||||
|
@ -476,26 +482,44 @@ export default class CallHandler extends EventEmitter {
|
|||
}
|
||||
|
||||
switch (newState) {
|
||||
case CallState.Ringing:
|
||||
this.play(AudioID.Ring);
|
||||
case CallState.Ringing: {
|
||||
const incomingCallPushRule = (
|
||||
new PushProcessor(MatrixClientPeg.get()).getPushRuleById(RuleId.IncomingCall) as IPushRule
|
||||
);
|
||||
const pushRuleEnabled = incomingCallPushRule?.enabled;
|
||||
const tweakSetToRing = incomingCallPushRule?.actions.some((action: Tweaks) => (
|
||||
action.set_tweak === TweakName.Sound &&
|
||||
action.value === "ring"
|
||||
));
|
||||
|
||||
if (pushRuleEnabled && tweakSetToRing) {
|
||||
this.play(AudioID.Ring);
|
||||
} else {
|
||||
this.silenceCall(call.callId);
|
||||
}
|
||||
break;
|
||||
case CallState.InviteSent:
|
||||
}
|
||||
case CallState.InviteSent: {
|
||||
this.play(AudioID.Ringback);
|
||||
break;
|
||||
case CallState.Ended:
|
||||
{
|
||||
}
|
||||
case CallState.Ended: {
|
||||
const hangupReason = call.hangupReason;
|
||||
Analytics.trackEvent('voip', 'callEnded', 'hangupReason', hangupReason);
|
||||
this.removeCallForRoom(mappedRoomId);
|
||||
if (oldState === CallState.InviteSent && call.hangupParty === CallParty.Remote) {
|
||||
this.play(AudioID.Busy);
|
||||
|
||||
// Don't show a modal when we got rejected/the call was hung up
|
||||
if (!hangupReason || [CallErrorCode.UserHangup, "user hangup"].includes(hangupReason)) break;
|
||||
|
||||
let title;
|
||||
let description;
|
||||
// TODO: We should either do away with these or figure out a copy for each code (expect user_hangup...)
|
||||
if (call.hangupReason === CallErrorCode.UserBusy) {
|
||||
title = _t("User Busy");
|
||||
description = _t("The user you called is busy.");
|
||||
} else if (hangupReason && ![CallErrorCode.UserHangup, "user hangup"].includes(hangupReason)) {
|
||||
} else {
|
||||
title = _t("Call Failed");
|
||||
description = _t("The call could not be established");
|
||||
}
|
||||
|
@ -624,6 +648,19 @@ export default class CallHandler extends EventEmitter {
|
|||
`Call state in ${mappedRoomId} changed to ${status}`,
|
||||
);
|
||||
|
||||
const toastKey = getIncomingCallToastKey(call.callId);
|
||||
if (status === CallState.Ringing) {
|
||||
ToastStore.sharedInstance().addOrReplaceToast({
|
||||
key: toastKey,
|
||||
priority: 100,
|
||||
component: IncomingCallToast,
|
||||
bodyClassName: "mx_IncomingCallToast",
|
||||
props: { call },
|
||||
});
|
||||
} else {
|
||||
ToastStore.sharedInstance().dismissToast(toastKey);
|
||||
}
|
||||
|
||||
dis.dispatch({
|
||||
action: 'call_state',
|
||||
room_id: mappedRoomId,
|
||||
|
@ -914,6 +951,8 @@ export default class CallHandler extends EventEmitter {
|
|||
action: 'view_room',
|
||||
room_id: roomId,
|
||||
});
|
||||
|
||||
await this.placeCall(roomId, PlaceCallType.Voice, null);
|
||||
}
|
||||
|
||||
private async startTransferToPhoneNumber(call: MatrixCall, destination: string, consultFirst: boolean) {
|
||||
|
@ -993,14 +1032,10 @@ export default class CallHandler extends EventEmitter {
|
|||
|
||||
// prevent double clicking the call button
|
||||
const room = MatrixClientPeg.get().getRoom(roomId);
|
||||
const currentJitsiWidgets = WidgetUtils.getRoomWidgetsOfType(room, WidgetType.JITSI);
|
||||
const hasJitsi = currentJitsiWidgets.length > 0
|
||||
|| WidgetEchoStore.roomHasPendingWidgetsOfType(roomId, currentJitsiWidgets, WidgetType.JITSI);
|
||||
if (hasJitsi) {
|
||||
Modal.createTrackedDialog('Call already in progress', '', ErrorDialog, {
|
||||
title: _t('Call in Progress'),
|
||||
description: _t('A call is currently being placed!'),
|
||||
});
|
||||
const jitsiWidget = WidgetStore.instance.getApps(roomId).find((app) => WidgetType.JITSI.matches(app.type));
|
||||
if (jitsiWidget) {
|
||||
// If there already is a Jitsi widget pin it
|
||||
WidgetLayoutStore.instance.moveToContainer(room, jitsiWidget, Container.Top);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -209,6 +209,14 @@ async function loadImageElement(imageFile: File) {
|
|||
return { width, height, img };
|
||||
}
|
||||
|
||||
// Minimum size for image files before we generate a thumbnail for them.
|
||||
const IMAGE_SIZE_THRESHOLD_THUMBNAIL = 1 << 15; // 32KB
|
||||
// Minimum size improvement for image thumbnails, if both are not met then don't bother uploading thumbnail.
|
||||
const IMAGE_THUMBNAIL_MIN_REDUCTION_SIZE = 1 << 16; // 1MB
|
||||
const IMAGE_THUMBNAIL_MIN_REDUCTION_PERCENT = 0.1; // 10%
|
||||
// We don't apply these thresholds to video thumbnails as a poster image is always useful
|
||||
// and videos tend to be much larger.
|
||||
|
||||
/**
|
||||
* Read the metadata for an image file and create and upload a thumbnail of the image.
|
||||
*
|
||||
|
@ -217,23 +225,33 @@ async function loadImageElement(imageFile: File) {
|
|||
* @param {File} imageFile The image to read and thumbnail.
|
||||
* @return {Promise} A promise that resolves with the attachment info.
|
||||
*/
|
||||
function infoForImageFile(matrixClient, roomId, imageFile) {
|
||||
async function infoForImageFile(matrixClient: MatrixClient, roomId: string, imageFile: File) {
|
||||
let thumbnailType = "image/png";
|
||||
if (imageFile.type === "image/jpeg") {
|
||||
thumbnailType = "image/jpeg";
|
||||
}
|
||||
|
||||
let imageInfo;
|
||||
return loadImageElement(imageFile).then((r) => {
|
||||
return createThumbnail(r.img, r.width, r.height, thumbnailType);
|
||||
}).then((result) => {
|
||||
imageInfo = result.info;
|
||||
return uploadFile(matrixClient, roomId, result.thumbnail);
|
||||
}).then((result) => {
|
||||
imageInfo.thumbnail_url = result.url;
|
||||
imageInfo.thumbnail_file = result.file;
|
||||
const imageElement = await loadImageElement(imageFile);
|
||||
|
||||
const result = await createThumbnail(imageElement.img, imageElement.width, imageElement.height, thumbnailType);
|
||||
const imageInfo = result.info;
|
||||
|
||||
// we do all sizing checks here because we still rely on thumbnail generation for making a blurhash from.
|
||||
const sizeDifference = imageFile.size - imageInfo.thumbnail_info.size;
|
||||
if (
|
||||
imageFile.size <= IMAGE_SIZE_THRESHOLD_THUMBNAIL || // image is small enough already
|
||||
(sizeDifference <= IMAGE_THUMBNAIL_MIN_REDUCTION_SIZE && // thumbnail is not sufficiently smaller than original
|
||||
sizeDifference <= (imageFile.size * IMAGE_THUMBNAIL_MIN_REDUCTION_PERCENT))
|
||||
) {
|
||||
delete imageInfo["thumbnail_info"];
|
||||
return imageInfo;
|
||||
});
|
||||
}
|
||||
|
||||
const uploadResult = await uploadFile(matrixClient, roomId, result.thumbnail);
|
||||
|
||||
imageInfo["thumbnail_url"] = uploadResult.url;
|
||||
imageInfo["thumbnail_file"] = uploadResult.file;
|
||||
return imageInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -123,6 +123,19 @@ export function formatTime(date: Date, showTwelveHour = false): string {
|
|||
return pad(date.getHours()) + ':' + pad(date.getMinutes());
|
||||
}
|
||||
|
||||
export function formatCallTime(delta: Date): string {
|
||||
const hours = delta.getUTCHours();
|
||||
const minutes = delta.getUTCMinutes();
|
||||
const seconds = delta.getUTCSeconds();
|
||||
|
||||
let output = "";
|
||||
if (hours) output += `${hours}h `;
|
||||
if (minutes || output) output += `${minutes}m `;
|
||||
if (seconds || output) output += `${seconds}s`;
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
const MILLIS_IN_DAY = 86400000;
|
||||
export function wantsDateSeparator(prevEventDate: Date, nextEventDate: Date): boolean {
|
||||
if (!nextEventDate || !prevEventDate) {
|
||||
|
|
|
@ -57,7 +57,33 @@ const BIGEMOJI_REGEX = new RegExp(`^(${EMOJIBASE_REGEX.source})+$`, 'i');
|
|||
|
||||
const COLOR_REGEX = /^#[0-9a-fA-F]{6}$/;
|
||||
|
||||
export const PERMITTED_URL_SCHEMES = ['http', 'https', 'ftp', 'mailto', 'magnet', 'matrix'];
|
||||
export const PERMITTED_URL_SCHEMES = [
|
||||
"bitcoin",
|
||||
"ftp",
|
||||
"geo",
|
||||
"http",
|
||||
"https",
|
||||
"im",
|
||||
"irc",
|
||||
"ircs",
|
||||
"magnet",
|
||||
"mailto",
|
||||
"matrix",
|
||||
"mms",
|
||||
"news",
|
||||
"nntp",
|
||||
"openpgp4fpr",
|
||||
"sip",
|
||||
"sftp",
|
||||
"sms",
|
||||
"smsto",
|
||||
"ssh",
|
||||
"tel",
|
||||
"urn",
|
||||
"webcal",
|
||||
"wtai",
|
||||
"xmpp",
|
||||
];
|
||||
|
||||
const MEDIA_API_MXC_REGEX = /\/_matrix\/media\/r0\/(?:download|thumbnail)\/(.+?)\/(.+?)(?:[?/]|$)/;
|
||||
|
||||
|
|
|
@ -146,23 +146,23 @@ export default class IdentityAuthClient {
|
|||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
const { finished } = Modal.createTrackedDialog('Default identity server terms warning', '',
|
||||
QuestionDialog, {
|
||||
title: _t("Identity server has no terms of service"),
|
||||
description: (
|
||||
<div>
|
||||
<p>{ _t(
|
||||
"This action requires accessing the default identity server " +
|
||||
title: _t("Identity server has no terms of service"),
|
||||
description: (
|
||||
<div>
|
||||
<p>{ _t(
|
||||
"This action requires accessing the default identity server " +
|
||||
"<server /> to validate an email address or phone number, " +
|
||||
"but the server does not have any terms of service.", {},
|
||||
{
|
||||
server: () => <b>{ abbreviateUrl(identityServerUrl) }</b>,
|
||||
},
|
||||
) }</p>
|
||||
<p>{ _t(
|
||||
"Only continue if you trust the owner of the server.",
|
||||
) }</p>
|
||||
</div>
|
||||
),
|
||||
button: _t("Trust"),
|
||||
{
|
||||
server: () => <b>{ abbreviateUrl(identityServerUrl) }</b>,
|
||||
},
|
||||
) }</p>
|
||||
<p>{ _t(
|
||||
"Only continue if you trust the owner of the server.",
|
||||
) }</p>
|
||||
</div>
|
||||
),
|
||||
button: _t("Trust"),
|
||||
});
|
||||
const [confirmed] = await finished;
|
||||
if (confirmed) {
|
||||
|
|
|
@ -163,7 +163,7 @@ const shortcuts: Record<Categories, IShortcut[]> = {
|
|||
modifiers: [Modifiers.SHIFT],
|
||||
key: Key.PAGE_UP,
|
||||
}],
|
||||
description: _td("Jump to oldest unread message"),
|
||||
description: _td("Jump to oldest unread message"),
|
||||
}, {
|
||||
keybinds: [{
|
||||
modifiers: [CMD_OR_CTRL, Modifiers.SHIFT],
|
||||
|
|
|
@ -38,17 +38,9 @@ function makePlaybackWaveform(input: number[]): number[] {
|
|||
// First, convert negative amplitudes to positive so we don't detect zero as "noisy".
|
||||
const noiseWaveform = input.map(v => Math.abs(v));
|
||||
|
||||
// Next, we'll resample the waveform using a smoothing approach so we can keep the same rough shape.
|
||||
// We also rescale the waveform to be 0-1 for the remaining function logic.
|
||||
const resampled = arrayRescale(arraySmoothingResample(noiseWaveform, PLAYBACK_WAVEFORM_SAMPLES), 0, 1);
|
||||
|
||||
// Then, we'll do a high and low pass filter to isolate actual speaking volumes within the rescaled
|
||||
// waveform. Most speech happens below the 0.5 mark.
|
||||
const filtered = resampled.map(v => clamp(v, 0.1, 0.5));
|
||||
|
||||
// Finally, we'll rescale the filtered waveform (0.1-0.5 becomes 0-1 again) so the user sees something
|
||||
// sensible. This is what we return to keep our contract of "values between zero and one".
|
||||
return arrayRescale(filtered, 0, 1);
|
||||
// Then, we'll resample the waveform using a smoothing approach so we can keep the same rough shape.
|
||||
// We also rescale the waveform to be 0-1 so we end up with a clamped waveform to rely upon.
|
||||
return arrayRescale(arraySmoothingResample(noiseWaveform, PLAYBACK_WAVEFORM_SAMPLES), 0, 1);
|
||||
}
|
||||
|
||||
export class Playback extends EventEmitter implements IDestroyable {
|
||||
|
|
|
@ -30,6 +30,7 @@ import { IEncryptedFile } from "matrix-js-sdk/src/@types/event";
|
|||
import { uploadFile } from "../ContentMessages";
|
||||
import { FixedRollingArray } from "../utils/FixedRollingArray";
|
||||
import { clamp } from "../utils/numbers";
|
||||
import mxRecorderWorkletPath from "./RecorderWorklet";
|
||||
|
||||
const CHANNELS = 1; // stereo isn't important
|
||||
export const SAMPLE_RATE = 48000; // 48khz is what WebRTC uses. 12khz is where we lose quality.
|
||||
|
@ -113,16 +114,10 @@ export class VoiceRecording extends EventEmitter implements IDestroyable {
|
|||
});
|
||||
this.recorderSource = this.recorderContext.createMediaStreamSource(this.recorderStream);
|
||||
|
||||
// Set up our worklet. We use this for timing information and waveform analysis: the
|
||||
// web audio API prefers this be done async to avoid holding the main thread with math.
|
||||
const mxRecorderWorkletPath = document.body.dataset.vectorRecorderWorkletScript;
|
||||
if (!mxRecorderWorkletPath) {
|
||||
// noinspection ExceptionCaughtLocallyJS
|
||||
throw new Error("Unable to create recorder: no worklet script registered");
|
||||
}
|
||||
|
||||
// Connect our inputs and outputs
|
||||
if (this.recorderContext.audioWorklet) {
|
||||
// Set up our worklet. We use this for timing information and waveform analysis: the
|
||||
// web audio API prefers this be done async to avoid holding the main thread with math.
|
||||
await this.recorderContext.audioWorklet.addModule(mxRecorderWorkletPath);
|
||||
this.recorderWorklet = new AudioWorkletNode(this.recorderContext, WORKLET_NAME);
|
||||
this.recorderSource.connect(this.recorderWorklet);
|
||||
|
|
|
@ -27,9 +27,15 @@ export enum CallEventGrouperEvent {
|
|||
SilencedChanged = "silenced_changed",
|
||||
}
|
||||
|
||||
const CONNECTING_STATES = [
|
||||
CallState.Connecting,
|
||||
CallState.WaitLocalMedia,
|
||||
CallState.CreateOffer,
|
||||
CallState.CreateAnswer,
|
||||
];
|
||||
|
||||
const SUPPORTED_STATES = [
|
||||
CallState.Connected,
|
||||
CallState.Connecting,
|
||||
CallState.Ringing,
|
||||
];
|
||||
|
||||
|
@ -61,6 +67,10 @@ export default class CallEventGrouper extends EventEmitter {
|
|||
return [...this.events].find((event) => event.getType() === EventType.CallReject);
|
||||
}
|
||||
|
||||
private get selectAnswer(): MatrixEvent {
|
||||
return [...this.events].find((event) => event.getType() === EventType.CallSelectAnswer);
|
||||
}
|
||||
|
||||
public get isVoice(): boolean {
|
||||
const invite = this.invite;
|
||||
if (!invite) return;
|
||||
|
@ -82,6 +92,11 @@ export default class CallEventGrouper extends EventEmitter {
|
|||
return Boolean(this.reject);
|
||||
}
|
||||
|
||||
public get duration(): Date {
|
||||
if (!this.hangup || !this.selectAnswer) return;
|
||||
return new Date(this.hangup.getDate().getTime() - this.selectAnswer.getDate().getTime());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if there are only events from the other side - we missed the call
|
||||
*/
|
||||
|
@ -127,7 +142,9 @@ export default class CallEventGrouper extends EventEmitter {
|
|||
}
|
||||
|
||||
private setState = () => {
|
||||
if (SUPPORTED_STATES.includes(this.call?.state)) {
|
||||
if (CONNECTING_STATES.includes(this.call?.state)) {
|
||||
this.state = CallState.Connecting;
|
||||
} else if (SUPPORTED_STATES.includes(this.call?.state)) {
|
||||
this.state = this.call.state;
|
||||
} else {
|
||||
if (this.callWasMissed) this.state = CustomCallState.Missed;
|
||||
|
|
|
@ -51,7 +51,12 @@ import EditorStateTransfer from "../../utils/EditorStateTransfer";
|
|||
|
||||
const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes
|
||||
const continuedTypes = [EventType.Sticker, EventType.RoomMessage];
|
||||
const membershipTypes = [EventType.RoomMember, EventType.RoomThirdPartyInvite, EventType.RoomServerAcl];
|
||||
const groupedEvents = [
|
||||
EventType.RoomMember,
|
||||
EventType.RoomThirdPartyInvite,
|
||||
EventType.RoomServerAcl,
|
||||
EventType.RoomPinnedEvents,
|
||||
];
|
||||
|
||||
// check if there is a previous event and it has the same sender as this event
|
||||
// and the types are the same/is in continuedTypes and the time between them is <= CONTINUATION_MAX_INTERVAL
|
||||
|
@ -1234,7 +1239,7 @@ class RedactionGrouper extends BaseGrouper {
|
|||
// Wrap consecutive member events in a ListSummary, ignore if redacted
|
||||
class MemberGrouper extends BaseGrouper {
|
||||
static canStartGroup = function(panel: MessagePanel, ev: MatrixEvent): boolean {
|
||||
return panel.shouldShowEvent(ev) && membershipTypes.includes(ev.getType() as EventType);
|
||||
return panel.shouldShowEvent(ev) && groupedEvents.includes(ev.getType() as EventType);
|
||||
};
|
||||
|
||||
constructor(
|
||||
|
@ -1252,7 +1257,7 @@ class MemberGrouper extends BaseGrouper {
|
|||
if (this.panel.wantsDateSeparator(this.events[0], ev.getDate())) {
|
||||
return false;
|
||||
}
|
||||
return membershipTypes.includes(ev.getType() as EventType);
|
||||
return groupedEvents.includes(ev.getType() as EventType);
|
||||
}
|
||||
|
||||
public add(ev: MatrixEvent, showHiddenEvents?: boolean): void {
|
||||
|
|
|
@ -192,11 +192,11 @@ const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) =>
|
|||
|
||||
if (inviteSender) {
|
||||
inviterSection = <div className="mx_SpaceRoomView_preview_inviter">
|
||||
<MemberAvatar member={inviter} width={32} height={32} />
|
||||
<MemberAvatar member={inviter} fallbackUserId={inviteSender} width={32} height={32} />
|
||||
<div>
|
||||
<div className="mx_SpaceRoomView_preview_inviter_name">
|
||||
{ _t("<inviter/> invites you", {}, {
|
||||
inviter: () => <b>{ inviter.name || inviteSender }</b>,
|
||||
inviter: () => <b>{ inviter?.name || inviteSender }</b>,
|
||||
}) }
|
||||
</div>
|
||||
{ inviter ? <div className="mx_SpaceRoomView_preview_inviter_mxid">
|
||||
|
|
|
@ -757,16 +757,20 @@ class TimelinePanel extends React.Component<IProps, IState> {
|
|||
}
|
||||
this.lastRMSentEventId = this.state.readMarkerEventId;
|
||||
|
||||
const roomId = this.props.timelineSet.room.roomId;
|
||||
const hiddenRR = SettingsStore.getValue("feature_hidden_read_receipts", roomId);
|
||||
|
||||
debuglog('TimelinePanel: Sending Read Markers for ',
|
||||
this.props.timelineSet.room.roomId,
|
||||
'rm', this.state.readMarkerEventId,
|
||||
lastReadEvent ? 'rr ' + lastReadEvent.getId() : '',
|
||||
' hidden:' + hiddenRR,
|
||||
);
|
||||
MatrixClientPeg.get().setRoomReadMarkers(
|
||||
this.props.timelineSet.room.roomId,
|
||||
roomId,
|
||||
this.state.readMarkerEventId,
|
||||
lastReadEvent, // Could be null, in which case no RR is sent
|
||||
{},
|
||||
{ hidden: hiddenRR },
|
||||
).catch((e) => {
|
||||
// /read_markers API is not implemented on this HS, fallback to just RR
|
||||
if (e.errcode === 'M_UNRECOGNIZED' && lastReadEvent) {
|
||||
|
|
|
@ -58,28 +58,39 @@ export default class ToastContainer extends React.Component<{}, IState> {
|
|||
let containerClasses;
|
||||
if (totalCount !== 0) {
|
||||
const topToast = this.state.toasts[0];
|
||||
const { title, icon, key, component, className, props } = topToast;
|
||||
const toastClasses = classNames("mx_Toast_toast", {
|
||||
const { title, icon, key, component, className, bodyClassName, props } = topToast;
|
||||
const bodyClasses = classNames("mx_Toast_body", bodyClassName);
|
||||
const toastClasses = classNames("mx_Toast_toast", className, {
|
||||
"mx_Toast_hasIcon": icon,
|
||||
[`mx_Toast_icon_${icon}`]: icon,
|
||||
}, className);
|
||||
|
||||
let countIndicator;
|
||||
if (isStacked || this.state.countSeen > 0) {
|
||||
countIndicator = ` (${this.state.countSeen + 1}/${this.state.countSeen + totalCount})`;
|
||||
}
|
||||
|
||||
});
|
||||
const toastProps = Object.assign({}, props, {
|
||||
key,
|
||||
toastKey: key,
|
||||
});
|
||||
toast = (<div className={toastClasses}>
|
||||
<div className="mx_Toast_title">
|
||||
<h2>{ title }</h2>
|
||||
<span>{ countIndicator }</span>
|
||||
const content = React.createElement(component, toastProps);
|
||||
|
||||
let countIndicator;
|
||||
if (title && isStacked || this.state.countSeen > 0) {
|
||||
countIndicator = ` (${this.state.countSeen + 1}/${this.state.countSeen + totalCount})`;
|
||||
}
|
||||
|
||||
let titleElement;
|
||||
if (title) {
|
||||
titleElement = (
|
||||
<div className="mx_Toast_title">
|
||||
<h2>{ title }</h2>
|
||||
<span>{ countIndicator }</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
toast = (
|
||||
<div className={toastClasses}>
|
||||
{ titleElement }
|
||||
<div className={bodyClasses}>{ content }</div>
|
||||
</div>
|
||||
<div className="mx_Toast_body">{ React.createElement(component, toastProps) }</div>
|
||||
</div>);
|
||||
);
|
||||
|
||||
containerClasses = classNames("mx_ToastContainer", {
|
||||
"mx_ToastContainer_stacked": isStacked,
|
||||
|
|
|
@ -17,8 +17,7 @@ limitations under the License.
|
|||
import React from "react";
|
||||
import { IRecordingUpdate, RECORDING_PLAYBACK_SAMPLES, VoiceRecording } from "../../../audio/VoiceRecording";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { arrayFastResample } from "../../../utils/arrays";
|
||||
import { percentageOf } from "../../../utils/numbers";
|
||||
import { arrayFastResample, arraySeed } from "../../../utils/arrays";
|
||||
import Waveform from "./Waveform";
|
||||
import { MarkedExecution } from "../../../utils/MarkedExecution";
|
||||
|
||||
|
@ -48,18 +47,14 @@ export default class LiveRecordingWaveform extends React.PureComponent<IProps, I
|
|||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
waveform: [],
|
||||
waveform: arraySeed(0, RECORDING_PLAYBACK_SAMPLES),
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.recorder.liveData.onUpdate((update: IRecordingUpdate) => {
|
||||
const bars = arrayFastResample(Array.from(update.waveform), RECORDING_PLAYBACK_SAMPLES);
|
||||
// The incoming data is between zero and one, but typically even screaming into a
|
||||
// microphone won't send you over 0.6, so we artificially adjust the gain for the
|
||||
// waveform. This results in a slightly more cinematic/animated waveform for the
|
||||
// user.
|
||||
this.waveform = bars.map(b => percentageOf(b, 0, 0.50));
|
||||
// The incoming data is between zero and one, so we don't need to clamp/rescale it.
|
||||
this.waveform = arrayFastResample(Array.from(update.waveform), RECORDING_PLAYBACK_SAMPLES);
|
||||
this.scheduledUpdate.mark();
|
||||
});
|
||||
}
|
||||
|
|
|
@ -490,9 +490,9 @@ class RoomStateExplorer extends React.PureComponent<IExplorerProps, IRoomStateEx
|
|||
forceStateEvent={true}
|
||||
onBack={this.onBack}
|
||||
inputs={{
|
||||
eventType: this.state.event.getType(),
|
||||
evContent: JSON.stringify(this.state.event.getContent(), null, '\t'),
|
||||
stateKey: this.state.event.getStateKey(),
|
||||
eventType: this.state.event.getType(),
|
||||
evContent: JSON.stringify(this.state.event.getContent(), null, '\t'),
|
||||
stateKey: this.state.event.getStateKey(),
|
||||
}}
|
||||
/>;
|
||||
}
|
||||
|
|
|
@ -20,10 +20,10 @@ import { _t } from '../../../languageHandler';
|
|||
import SdkConfig from '../../../SdkConfig';
|
||||
|
||||
export default function KeySignatureUploadFailedDialog({
|
||||
failures,
|
||||
source,
|
||||
continuation,
|
||||
onFinished,
|
||||
failures,
|
||||
source,
|
||||
continuation,
|
||||
onFinished,
|
||||
}) {
|
||||
const RETRIES = 2;
|
||||
const BaseDialog = sdk.getComponent('dialogs.BaseDialog');
|
||||
|
|
|
@ -16,6 +16,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { IKeyBackupInfo } from "matrix-js-sdk/src/crypto/keybackup";
|
||||
import Modal from '../../../Modal';
|
||||
import * as sdk from '../../../index';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
|
@ -24,19 +25,25 @@ import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
|||
import RestoreKeyBackupDialog from './security/RestoreKeyBackupDialog';
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
|
||||
interface IProps {
|
||||
onFinished: (success: boolean) => void;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
shouldLoadBackupStatus: boolean;
|
||||
loading: boolean;
|
||||
backupInfo: IKeyBackupInfo;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.dialogs.LogoutDialog")
|
||||
export default class LogoutDialog extends React.Component {
|
||||
defaultProps = {
|
||||
export default class LogoutDialog extends React.Component<IProps, IState> {
|
||||
static defaultProps = {
|
||||
onFinished: function() {},
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this._onSettingsLinkClick = this._onSettingsLinkClick.bind(this);
|
||||
this._onExportE2eKeysClicked = this._onExportE2eKeysClicked.bind(this);
|
||||
this._onFinished = this._onFinished.bind(this);
|
||||
this._onSetRecoveryMethodClick = this._onSetRecoveryMethodClick.bind(this);
|
||||
this._onLogoutConfirm = this._onLogoutConfirm.bind(this);
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
const cli = MatrixClientPeg.get();
|
||||
const shouldLoadBackupStatus = cli.isCryptoEnabled() && !cli.getKeyBackupEnabled();
|
||||
|
@ -49,11 +56,11 @@ export default class LogoutDialog extends React.Component {
|
|||
};
|
||||
|
||||
if (shouldLoadBackupStatus) {
|
||||
this._loadBackupStatus();
|
||||
this.loadBackupStatus();
|
||||
}
|
||||
}
|
||||
|
||||
async _loadBackupStatus() {
|
||||
private async loadBackupStatus() {
|
||||
try {
|
||||
const backupInfo = await MatrixClientPeg.get().getKeyBackupVersion();
|
||||
this.setState({
|
||||
|
@ -69,29 +76,29 @@ export default class LogoutDialog extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
_onSettingsLinkClick() {
|
||||
private onSettingsLinkClick = (): void => {
|
||||
// close dialog
|
||||
this.props.onFinished();
|
||||
}
|
||||
this.props.onFinished(true);
|
||||
};
|
||||
|
||||
_onExportE2eKeysClicked() {
|
||||
private onExportE2eKeysClicked = (): void => {
|
||||
Modal.createTrackedDialogAsync('Export E2E Keys', '',
|
||||
import('../../../async-components/views/dialogs/security/ExportE2eKeysDialog'),
|
||||
{
|
||||
matrixClient: MatrixClientPeg.get(),
|
||||
},
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
_onFinished(confirmed) {
|
||||
private onFinished = (confirmed: boolean): void => {
|
||||
if (confirmed) {
|
||||
dis.dispatch({ action: 'logout' });
|
||||
}
|
||||
// close dialog
|
||||
this.props.onFinished();
|
||||
}
|
||||
this.props.onFinished(confirmed);
|
||||
};
|
||||
|
||||
_onSetRecoveryMethodClick() {
|
||||
private onSetRecoveryMethodClick = (): void => {
|
||||
if (this.state.backupInfo) {
|
||||
// A key backup exists for this account, but the creating device is not
|
||||
// verified, so restore the backup which will give us the keys from it and
|
||||
|
@ -108,15 +115,15 @@ export default class LogoutDialog extends React.Component {
|
|||
}
|
||||
|
||||
// close dialog
|
||||
this.props.onFinished();
|
||||
}
|
||||
this.props.onFinished(true);
|
||||
};
|
||||
|
||||
_onLogoutConfirm() {
|
||||
private onLogoutConfirm = (): void => {
|
||||
dis.dispatch({ action: 'logout' });
|
||||
|
||||
// close dialog
|
||||
this.props.onFinished();
|
||||
}
|
||||
this.props.onFinished(true);
|
||||
};
|
||||
|
||||
render() {
|
||||
if (this.state.shouldLoadBackupStatus) {
|
||||
|
@ -152,16 +159,16 @@ export default class LogoutDialog extends React.Component {
|
|||
</div>
|
||||
<DialogButtons primaryButton={setupButtonCaption}
|
||||
hasCancel={false}
|
||||
onPrimaryButtonClick={this._onSetRecoveryMethodClick}
|
||||
onPrimaryButtonClick={this.onSetRecoveryMethodClick}
|
||||
focus={true}
|
||||
>
|
||||
<button onClick={this._onLogoutConfirm}>
|
||||
<button onClick={this.onLogoutConfirm}>
|
||||
{ _t("I don't want my encrypted messages") }
|
||||
</button>
|
||||
</DialogButtons>
|
||||
<details>
|
||||
<summary>{ _t("Advanced") }</summary>
|
||||
<p><button onClick={this._onExportE2eKeysClicked}>
|
||||
<p><button onClick={this.onExportE2eKeysClicked}>
|
||||
{ _t("Manually export keys") }
|
||||
</button></p>
|
||||
</details>
|
||||
|
@ -174,7 +181,7 @@ export default class LogoutDialog extends React.Component {
|
|||
title={_t("You'll lose access to your encrypted messages")}
|
||||
contentId='mx_Dialog_content'
|
||||
hasCancel={true}
|
||||
onFinished={this._onFinished}
|
||||
onFinished={this.onFinished}
|
||||
>
|
||||
{ dialogContent }
|
||||
</BaseDialog>);
|
||||
|
@ -187,7 +194,7 @@ export default class LogoutDialog extends React.Component {
|
|||
"Are you sure you want to sign out?",
|
||||
)}
|
||||
button={_t("Sign out")}
|
||||
onFinished={this._onFinished}
|
||||
onFinished={this.onFinished}
|
||||
/>);
|
||||
}
|
||||
}
|
|
@ -62,10 +62,10 @@ export default class AppPermission extends React.Component<IProps, IState> {
|
|||
|
||||
// Set all this into the initial state
|
||||
this.state = {
|
||||
...urlInfo,
|
||||
roomMember,
|
||||
isWrapped: null,
|
||||
widgetDomain: null,
|
||||
isWrapped: null,
|
||||
roomMember,
|
||||
...urlInfo,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -34,7 +34,7 @@ interface IProps {
|
|||
// The list of room members for which to show avatars next to the summary
|
||||
summaryMembers?: RoomMember[];
|
||||
// The text to show as the summary of this event list
|
||||
summaryText?: string;
|
||||
summaryText?: string | JSX.Element;
|
||||
// An array of EventTiles to render when expanded
|
||||
children: ReactNode[];
|
||||
// Called when the event list expansion is toggled
|
||||
|
|
|
@ -25,8 +25,24 @@ import { formatCommaSeparatedList } from '../../../utils/FormattingUtils';
|
|||
import { isValid3pidInvite } from "../../../RoomInvite";
|
||||
import EventListSummary from "./EventListSummary";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import defaultDispatcher from '../../../dispatcher/dispatcher';
|
||||
import { RightPanelPhases } from '../../../stores/RightPanelStorePhases';
|
||||
import { Action } from '../../../dispatcher/actions';
|
||||
import { SetRightPanelPhasePayload } from '../../../dispatcher/payloads/SetRightPanelPhasePayload';
|
||||
import { jsxJoin } from '../../../utils/ReactUtils';
|
||||
import { EventType } from 'matrix-js-sdk/src/@types/event';
|
||||
import { Layout } from '../../../settings/Layout';
|
||||
|
||||
const onPinnedMessagesClick = (): void => {
|
||||
defaultDispatcher.dispatch<SetRightPanelPhasePayload>({
|
||||
action: Action.SetRightPanelPhase,
|
||||
phase: RightPanelPhases.PinnedMessages,
|
||||
allowClose: false,
|
||||
});
|
||||
};
|
||||
|
||||
const SENDER_AS_DISPLAY_NAME_EVENTS = [EventType.RoomServerAcl, EventType.RoomPinnedEvents];
|
||||
|
||||
interface IProps extends Omit<ComponentProps<typeof EventListSummary>, "summaryText" | "summaryMembers"> {
|
||||
// The maximum number of names to show in either each summary e.g. 2 would result "A, B and 234 others left"
|
||||
summaryLength?: number;
|
||||
|
@ -60,6 +76,7 @@ enum TransitionType {
|
|||
ChangedAvatar = "changed_avatar",
|
||||
NoChange = "no_change",
|
||||
ServerAcl = "server_acl",
|
||||
ChangedPins = "pinned_messages"
|
||||
}
|
||||
|
||||
const SEP = ",";
|
||||
|
@ -93,7 +110,10 @@ export default class MemberEventListSummary extends React.Component<IProps> {
|
|||
* `Object.keys(eventAggregates)`.
|
||||
* @returns {string} the textual summary of the aggregated events that occurred.
|
||||
*/
|
||||
private generateSummary(eventAggregates: Record<string, string[]>, orderedTransitionSequences: string[]) {
|
||||
private generateSummary(
|
||||
eventAggregates: Record<string, string[]>,
|
||||
orderedTransitionSequences: string[],
|
||||
): string | JSX.Element {
|
||||
const summaries = orderedTransitionSequences.map((transitions) => {
|
||||
const userNames = eventAggregates[transitions];
|
||||
const nameList = this.renderNameList(userNames);
|
||||
|
@ -122,7 +142,7 @@ export default class MemberEventListSummary extends React.Component<IProps> {
|
|||
return null;
|
||||
}
|
||||
|
||||
return summaries.join(", ");
|
||||
return jsxJoin(summaries, ", ");
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -216,7 +236,11 @@ export default class MemberEventListSummary extends React.Component<IProps> {
|
|||
* @param {number} repeats the number of times the transition was repeated in a row.
|
||||
* @returns {string} the written Human Readable equivalent of the transition.
|
||||
*/
|
||||
private static getDescriptionForTransition(t: TransitionType, userCount: number, repeats: number) {
|
||||
private static getDescriptionForTransition(
|
||||
t: TransitionType,
|
||||
userCount: number,
|
||||
repeats: number,
|
||||
): string | JSX.Element {
|
||||
// The empty interpolations 'severalUsers' and 'oneUser'
|
||||
// are there only to show translators to non-English languages
|
||||
// that the verb is conjugated to plural or singular Subject.
|
||||
|
@ -299,6 +323,15 @@ export default class MemberEventListSummary extends React.Component<IProps> {
|
|||
{ severalUsers: "", count: repeats })
|
||||
: _t("%(oneUser)schanged the server ACLs %(count)s times", { oneUser: "", count: repeats });
|
||||
break;
|
||||
case "pinned_messages":
|
||||
res = (userCount > 1)
|
||||
? _t("%(severalUsers)schanged the <a>pinned messages</a> for the room %(count)s times.",
|
||||
{ severalUsers: "", count: repeats },
|
||||
{ "a": (sub) => <a onClick={onPinnedMessagesClick}> { sub } </a> })
|
||||
: _t("%(oneUser)schanged the <a>pinned messages</a> for the room %(count)s times.",
|
||||
{ oneUser: "", count: repeats },
|
||||
{ "a": (sub) => <a onClick={onPinnedMessagesClick}> { sub } </a> });
|
||||
break;
|
||||
}
|
||||
|
||||
return res;
|
||||
|
@ -317,16 +350,18 @@ export default class MemberEventListSummary extends React.Component<IProps> {
|
|||
* if a transition is not recognised.
|
||||
*/
|
||||
private static getTransition(e: IUserEvents): TransitionType {
|
||||
if (e.mxEvent.getType() === 'm.room.third_party_invite') {
|
||||
const type = e.mxEvent.getType();
|
||||
|
||||
if (type === EventType.RoomThirdPartyInvite) {
|
||||
// Handle 3pid invites the same as invites so they get bundled together
|
||||
if (!isValid3pidInvite(e.mxEvent)) {
|
||||
return TransitionType.InviteWithdrawal;
|
||||
}
|
||||
return TransitionType.Invited;
|
||||
}
|
||||
|
||||
if (e.mxEvent.getType() === 'm.room.server_acl') {
|
||||
} else if (type === EventType.RoomServerAcl) {
|
||||
return TransitionType.ServerAcl;
|
||||
} else if (type === EventType.RoomPinnedEvents) {
|
||||
return TransitionType.ChangedPins;
|
||||
}
|
||||
|
||||
switch (e.mxEvent.getContent().membership) {
|
||||
|
@ -415,22 +450,23 @@ export default class MemberEventListSummary extends React.Component<IProps> {
|
|||
// Object mapping user IDs to an array of IUserEvents
|
||||
const userEvents: Record<string, IUserEvents[]> = {};
|
||||
eventsToRender.forEach((e, index) => {
|
||||
const userId = e.getType() === 'm.room.server_acl' ? e.getSender() : e.getStateKey();
|
||||
const type = e.getType();
|
||||
const userId = type === EventType.RoomServerAcl ? e.getSender() : e.getStateKey();
|
||||
// Initialise a user's events
|
||||
if (!userEvents[userId]) {
|
||||
userEvents[userId] = [];
|
||||
}
|
||||
|
||||
if (e.getType() === 'm.room.server_acl') {
|
||||
if (SENDER_AS_DISPLAY_NAME_EVENTS.includes(type as EventType)) {
|
||||
latestUserAvatarMember.set(userId, e.sender);
|
||||
} else if (e.target) {
|
||||
latestUserAvatarMember.set(userId, e.target);
|
||||
}
|
||||
|
||||
let displayName = userId;
|
||||
if (e.getType() === 'm.room.third_party_invite') {
|
||||
if (type === EventType.RoomThirdPartyInvite) {
|
||||
displayName = e.getContent().display_name;
|
||||
} else if (e.getType() === 'm.room.server_acl') {
|
||||
} else if (SENDER_AS_DISPLAY_NAME_EVENTS.includes(type as EventType)) {
|
||||
displayName = e.sender.name;
|
||||
} else if (e.target) {
|
||||
displayName = e.target.name;
|
||||
|
|
|
@ -25,7 +25,7 @@ import { CallErrorCode, CallState } from 'matrix-js-sdk/src/webrtc/call';
|
|||
import InfoTooltip, { InfoTooltipKind } from '../elements/InfoTooltip';
|
||||
import classNames from 'classnames';
|
||||
import AccessibleTooltipButton from '../elements/AccessibleTooltipButton';
|
||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
import { formatCallTime } from "../../../DateUtils";
|
||||
|
||||
interface IProps {
|
||||
mxEvent: MatrixEvent;
|
||||
|
@ -117,14 +117,12 @@ export default class CallEvent extends React.Component<IProps, IState> {
|
|||
if (state === CallState.Ended) {
|
||||
const hangupReason = this.props.callEventGrouper.hangupReason;
|
||||
const gotRejected = this.props.callEventGrouper.gotRejected;
|
||||
const rejectParty = this.props.callEventGrouper.rejectParty;
|
||||
|
||||
if (gotRejected) {
|
||||
const weDeclinedCall = MatrixClientPeg.get().getUserId() === rejectParty;
|
||||
return (
|
||||
<div className="mx_CallEvent_content">
|
||||
{ weDeclinedCall ? _t("You declined this call") : _t("They declined this call") }
|
||||
{ this.renderCallBackButton(weDeclinedCall ? _t("Call back") : _t("Call again")) }
|
||||
{ _t("Call declined") }
|
||||
{ this.renderCallBackButton(_t("Call back")) }
|
||||
</div>
|
||||
);
|
||||
} else if (([CallErrorCode.UserHangup, "user hangup"].includes(hangupReason) || !hangupReason)) {
|
||||
|
@ -134,16 +132,21 @@ export default class CallEvent extends React.Component<IProps, IState> {
|
|||
// https://github.com/vector-im/riot-android/issues/2623
|
||||
// Also the correct hangup code as of VoIP v1 (with underscore)
|
||||
// Also, if we don't have a reason
|
||||
const duration = this.props.callEventGrouper.duration;
|
||||
let text = _t("Call ended");
|
||||
if (duration) {
|
||||
text += " • " + formatCallTime(duration);
|
||||
}
|
||||
return (
|
||||
<div className="mx_CallEvent_content">
|
||||
{ _t("This call has ended") }
|
||||
{ text }
|
||||
</div>
|
||||
);
|
||||
} else if (hangupReason === CallErrorCode.InviteTimeout) {
|
||||
return (
|
||||
<div className="mx_CallEvent_content">
|
||||
{ _t("They didn't pick up") }
|
||||
{ this.renderCallBackButton(_t("Call again")) }
|
||||
{ _t("Missed call") }
|
||||
{ this.renderCallBackButton(_t("Call back")) }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -176,7 +179,8 @@ export default class CallEvent extends React.Component<IProps, IState> {
|
|||
className="mx_CallEvent_content_tooltip"
|
||||
kind={InfoTooltipKind.Warning}
|
||||
/>
|
||||
{ _t("This call has failed") }
|
||||
{ _t("Connection failed") }
|
||||
{ this.renderCallBackButton(_t("Retry")) }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -190,7 +194,7 @@ export default class CallEvent extends React.Component<IProps, IState> {
|
|||
if (state === CustomCallState.Missed) {
|
||||
return (
|
||||
<div className="mx_CallEvent_content">
|
||||
{ _t("You missed this call") }
|
||||
{ _t("Missed call") }
|
||||
{ this.renderCallBackButton(_t("Call back")) }
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -136,7 +136,8 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
|
|||
private addCodeExpansionButton(div: HTMLDivElement, pre: HTMLPreElement): void {
|
||||
// Calculate how many percent does the pre element take up.
|
||||
// If it's less than 30% we don't add the expansion button.
|
||||
const percentageOfViewport = pre.offsetHeight / UIStore.instance.windowHeight * 100;
|
||||
// We also round the number as it sometimes can be 29.99...
|
||||
const percentageOfViewport = Math.round(pre.offsetHeight / UIStore.instance.windowHeight * 100);
|
||||
if (percentageOfViewport < 30) return;
|
||||
|
||||
const button = document.createElement("span");
|
||||
|
|
|
@ -152,7 +152,7 @@ const PinnedMessagesCard = ({ room, onClose }: IProps) => {
|
|||
<h2>{ _t("Nothing pinned, yet") }</h2>
|
||||
{ _t("If you have permissions, open the menu on any message and select " +
|
||||
"<b>Pin</b> to stick them here.", {}, {
|
||||
b: sub => <b>{ sub }</b>,
|
||||
b: sub => <b>{ sub }</b>,
|
||||
}) }
|
||||
</div>
|
||||
</div>;
|
||||
|
|
|
@ -1381,8 +1381,8 @@ const BasicUserInfo: React.FC<{
|
|||
className="mx_UserInfo_field"
|
||||
onClick={() => {
|
||||
dis.dispatch({
|
||||
action: Action.ViewUserSettings,
|
||||
initialTabId: UserTab.Security,
|
||||
action: Action.ViewUserSettings,
|
||||
initialTabId: UserTab.Security,
|
||||
});
|
||||
}}
|
||||
>
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
/*
|
||||
Copyright 2019 New Vector Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2019 - 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -55,6 +54,14 @@ const REGEX_EMOTICON_WHITESPACE = new RegExp('(?:^|\\s)(' + EMOTICON_REGEX.sourc
|
|||
|
||||
const IS_MAC = navigator.platform.indexOf("Mac") !== -1;
|
||||
|
||||
const SURROUND_WITH_CHARACTERS = ["\"", "_", "`", "'", "*", "~", "$"];
|
||||
const SURROUND_WITH_DOUBLE_CHARACTERS = new Map([
|
||||
["(", ")"],
|
||||
["[", "]"],
|
||||
["{", "}"],
|
||||
["<", ">"],
|
||||
]);
|
||||
|
||||
function ctrlShortcutLabel(key: string): string {
|
||||
return (IS_MAC ? "⌘" : "Ctrl") + "+" + key;
|
||||
}
|
||||
|
@ -99,6 +106,7 @@ interface IState {
|
|||
showVisualBell?: boolean;
|
||||
autoComplete?: AutocompleteWrapperModel;
|
||||
completionIndex?: number;
|
||||
surroundWith: boolean;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.rooms.BasicMessageEditor")
|
||||
|
@ -117,12 +125,14 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
|||
|
||||
private readonly emoticonSettingHandle: string;
|
||||
private readonly shouldShowPillAvatarSettingHandle: string;
|
||||
private readonly surroundWithHandle: string;
|
||||
private readonly historyManager = new HistoryManager();
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
showPillAvatar: SettingsStore.getValue("Pill.shouldShowPillAvatar"),
|
||||
surroundWith: SettingsStore.getValue("MessageComposerInput.surroundWith"),
|
||||
};
|
||||
|
||||
this.emoticonSettingHandle = SettingsStore.watchSetting('MessageComposerInput.autoReplaceEmoji', null,
|
||||
|
@ -130,6 +140,8 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
|||
this.configureEmoticonAutoReplace();
|
||||
this.shouldShowPillAvatarSettingHandle = SettingsStore.watchSetting("Pill.shouldShowPillAvatar", null,
|
||||
this.configureShouldShowPillAvatar);
|
||||
this.surroundWithHandle = SettingsStore.watchSetting("MessageComposerInput.surroundWith", null,
|
||||
this.surroundWithSettingChanged);
|
||||
}
|
||||
|
||||
public componentDidUpdate(prevProps: IProps) {
|
||||
|
@ -422,6 +434,28 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
|||
private onKeyDown = (event: React.KeyboardEvent): void => {
|
||||
const model = this.props.model;
|
||||
let handled = false;
|
||||
|
||||
if (this.state.surroundWith && document.getSelection().type != "Caret") {
|
||||
// This surrounds the selected text with a character. This is
|
||||
// intentionally left out of the keybinding manager as the keybinds
|
||||
// here shouldn't be changeable
|
||||
|
||||
const selectionRange = getRangeForSelection(
|
||||
this.editorRef.current,
|
||||
this.props.model,
|
||||
document.getSelection(),
|
||||
);
|
||||
// trim the range as we want it to exclude leading/trailing spaces
|
||||
selectionRange.trim();
|
||||
|
||||
if ([...SURROUND_WITH_DOUBLE_CHARACTERS.keys(), ...SURROUND_WITH_CHARACTERS].includes(event.key)) {
|
||||
this.historyManager.ensureLastChangesPushed(this.props.model);
|
||||
this.modifiedFlag = true;
|
||||
toggleInlineFormat(selectionRange, event.key, SURROUND_WITH_DOUBLE_CHARACTERS.get(event.key));
|
||||
handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
const action = getKeyBindingsManager().getMessageComposerAction(event);
|
||||
switch (action) {
|
||||
case MessageComposerAction.FormatBold:
|
||||
|
@ -574,6 +608,11 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
|||
this.setState({ showPillAvatar });
|
||||
};
|
||||
|
||||
private surroundWithSettingChanged = () => {
|
||||
const surroundWith = SettingsStore.getValue("MessageComposerInput.surroundWith");
|
||||
this.setState({ surroundWith });
|
||||
};
|
||||
|
||||
componentWillUnmount() {
|
||||
document.removeEventListener("selectionchange", this.onSelectionChange);
|
||||
this.editorRef.current.removeEventListener("input", this.onInput, true);
|
||||
|
@ -581,6 +620,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
|||
this.editorRef.current.removeEventListener("compositionend", this.onCompositionEnd, true);
|
||||
SettingsStore.unwatchSetting(this.emoticonSettingHandle);
|
||||
SettingsStore.unwatchSetting(this.shouldShowPillAvatarSettingHandle);
|
||||
SettingsStore.unwatchSetting(this.surroundWithHandle);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
|
@ -684,7 +724,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
|||
<MessageComposerFormatBar ref={this.formatBarRef} onAction={this.onFormatAction} shortcuts={shortcuts} />
|
||||
<div
|
||||
className={classes}
|
||||
contentEditable="true"
|
||||
contentEditable={this.props.disabled ? null : true}
|
||||
tabIndex={0}
|
||||
onBlur={this.onBlur}
|
||||
onFocus={this.onFocus}
|
||||
|
|
|
@ -58,6 +58,7 @@ function ComposerAvatar(props: IComposerAvatarProps) {
|
|||
|
||||
interface ISendButtonProps {
|
||||
onClick: () => void;
|
||||
title?: string; // defaults to something generic
|
||||
}
|
||||
|
||||
function SendButton(props: ISendButtonProps) {
|
||||
|
@ -65,7 +66,7 @@ function SendButton(props: ISendButtonProps) {
|
|||
<AccessibleTooltipButton
|
||||
className="mx_MessageComposer_sendMessage"
|
||||
onClick={props.onClick}
|
||||
title={_t('Send message')}
|
||||
title={props.title ?? _t('Send message')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -401,7 +402,11 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
|||
|
||||
if (!this.state.isComposerEmpty || this.state.haveRecording) {
|
||||
controls.push(
|
||||
<SendButton key="controls_send" onClick={this.sendMessage} />,
|
||||
<SendButton
|
||||
key="controls_send"
|
||||
onClick={this.sendMessage}
|
||||
title={this.state.haveRecording ? _t("Send voice message") : undefined}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
} else if (this.state.tombstone) {
|
||||
|
|
|
@ -64,9 +64,9 @@ const NewRoomIntro = () => {
|
|||
height={AVATAR_SIZE}
|
||||
onClick={() => {
|
||||
defaultDispatcher.dispatch<ViewUserPayload>({
|
||||
action: Action.ViewUser,
|
||||
// XXX: We should be using a real member object and not assuming what the receiver wants.
|
||||
member: member || { userId: dmPartner } as User,
|
||||
action: Action.ViewUser,
|
||||
// XXX: We should be using a real member object and not assuming what the receiver wants.
|
||||
member: member || { userId: dmPartner } as User,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
|
|
|
@ -145,7 +145,7 @@ export default class ReadReceiptMarker extends React.PureComponent {
|
|||
if (oldInfo && oldInfo.left) {
|
||||
// start at the old height and in the old h pos
|
||||
startStyles.push({ top: startTopOffset+"px",
|
||||
left: toPx(oldInfo.left) });
|
||||
left: toPx(oldInfo.left) });
|
||||
}
|
||||
|
||||
startStyles.push({ top: startTopOffset+'px', left: '0' });
|
||||
|
@ -174,14 +174,14 @@ export default class ReadReceiptMarker extends React.PureComponent {
|
|||
title = _t(
|
||||
"Seen by %(userName)s at %(dateTime)s",
|
||||
{ userName: this.props.fallbackUserId,
|
||||
dateTime: dateString },
|
||||
dateTime: dateString },
|
||||
);
|
||||
} else {
|
||||
title = _t(
|
||||
"Seen by %(displayName)s (%(userName)s) at %(dateTime)s",
|
||||
{ displayName: this.props.member.rawDisplayName,
|
||||
userName: this.props.fallbackUserId,
|
||||
dateTime: dateString },
|
||||
userName: this.props.fallbackUserId,
|
||||
dateTime: dateString },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, { createRef } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
|
@ -38,6 +38,8 @@ interface IProps {
|
|||
|
||||
@replaceableComponent("views.rooms.ReplyTile")
|
||||
export default class ReplyTile extends React.PureComponent<IProps> {
|
||||
private anchorElement = createRef<HTMLAnchorElement>();
|
||||
|
||||
static defaultProps = {
|
||||
onHeightChanged: () => {},
|
||||
};
|
||||
|
@ -71,7 +73,11 @@ export default class ReplyTile extends React.PureComponent<IProps> {
|
|||
// Following a link within a reply should not dispatch the `view_room` action
|
||||
// so that the browser can direct the user to the correct location
|
||||
// The exception being the link wrapping the reply
|
||||
if (clickTarget.tagName.toLowerCase() !== "a" || clickTarget.closest("a") === null) {
|
||||
if (
|
||||
clickTarget.tagName.toLowerCase() !== "a" ||
|
||||
clickTarget.closest("a") === null ||
|
||||
clickTarget === this.anchorElement.current
|
||||
) {
|
||||
// This allows the permalink to be opened in a new tab/window or copied as
|
||||
// matrix.to, but also for it to enable routing within Riot when clicked.
|
||||
e.preventDefault();
|
||||
|
@ -141,7 +147,7 @@ export default class ReplyTile extends React.PureComponent<IProps> {
|
|||
|
||||
return (
|
||||
<div className={classes}>
|
||||
<a href={permalink} onClick={this.onClick}>
|
||||
<a href={permalink} onClick={this.onClick} ref={this.anchorElement}>
|
||||
{ sender }
|
||||
<EventTileType
|
||||
ref="tile"
|
||||
|
|
|
@ -17,10 +17,7 @@ limitations under the License.
|
|||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import React, { ReactNode } from "react";
|
||||
import {
|
||||
RecordingState,
|
||||
VoiceRecording,
|
||||
} from "../../../audio/VoiceRecording";
|
||||
import { IUpload, RecordingState, VoiceRecording } from "../../../audio/VoiceRecording";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import classNames from "classnames";
|
||||
|
@ -34,6 +31,11 @@ import { MsgType } from "matrix-js-sdk/src/@types/event";
|
|||
import Modal from "../../../Modal";
|
||||
import ErrorDialog from "../dialogs/ErrorDialog";
|
||||
import MediaDeviceHandler, { MediaDeviceKindEnum } from "../../../MediaDeviceHandler";
|
||||
import NotificationBadge from "./NotificationBadge";
|
||||
import { StaticNotificationState } from "../../../stores/notifications/StaticNotificationState";
|
||||
import { NotificationColor } from "../../../stores/notifications/NotificationColor";
|
||||
import InlineSpinner from "../elements/InlineSpinner";
|
||||
import { PlaybackManager } from "../../../audio/PlaybackManager";
|
||||
|
||||
interface IProps {
|
||||
room: Room;
|
||||
|
@ -42,6 +44,7 @@ interface IProps {
|
|||
interface IState {
|
||||
recorder?: VoiceRecording;
|
||||
recordingPhase?: RecordingState;
|
||||
didUploadFail?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -69,9 +72,19 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
|
|||
|
||||
await this.state.recorder.stop();
|
||||
|
||||
let upload: IUpload;
|
||||
try {
|
||||
const upload = await this.state.recorder.upload(this.props.room.roomId);
|
||||
upload = await this.state.recorder.upload(this.props.room.roomId);
|
||||
} catch (e) {
|
||||
console.error("Error uploading voice message:", e);
|
||||
|
||||
// Flag error and move on. The recording phase will be reset by the upload function.
|
||||
this.setState({ didUploadFail: true });
|
||||
|
||||
return; // don't dispose the recording: the user has a chance to re-upload
|
||||
}
|
||||
|
||||
try {
|
||||
// noinspection ES6MissingAwait - we don't care if it fails, it'll get queued.
|
||||
MatrixClientPeg.get().sendMessage(this.props.room.roomId, {
|
||||
"body": "Voice message",
|
||||
|
@ -104,12 +117,11 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
|
|||
"org.matrix.msc3245.voice": {}, // No content, this is a rendering hint
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("Error sending/uploading voice message:", e);
|
||||
Modal.createTrackedDialog('Upload failed', '', ErrorDialog, {
|
||||
title: _t('Upload Failed'),
|
||||
description: _t("The voice message failed to upload."),
|
||||
});
|
||||
return; // don't dispose the recording so the user can retry, maybe
|
||||
console.error("Error sending voice message:", e);
|
||||
|
||||
// Voice message should be in the timeline at this point, so let other things take care
|
||||
// of error handling. We also shouldn't need the recording anymore, so fall through to
|
||||
// disposal.
|
||||
}
|
||||
await this.disposeRecording();
|
||||
}
|
||||
|
@ -118,7 +130,7 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
|
|||
await VoiceRecordingStore.instance.disposeRecording();
|
||||
|
||||
// Reset back to no recording, which means no phase (ie: restart component entirely)
|
||||
this.setState({ recorder: null, recordingPhase: null });
|
||||
this.setState({ recorder: null, recordingPhase: null, didUploadFail: false });
|
||||
}
|
||||
|
||||
private onCancel = async () => {
|
||||
|
@ -166,6 +178,9 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
|
|||
}
|
||||
|
||||
try {
|
||||
// stop any noises which might be happening
|
||||
await PlaybackManager.instance.playOnly(null);
|
||||
|
||||
const recorder = VoiceRecordingStore.instance.startRecording();
|
||||
await recorder.start();
|
||||
|
||||
|
@ -200,7 +215,7 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
|
|||
}
|
||||
|
||||
public render(): ReactNode {
|
||||
let recordingInfo;
|
||||
let stopOrRecordBtn;
|
||||
let deleteButton;
|
||||
if (!this.state.recordingPhase || this.state.recordingPhase === RecordingState.Started) {
|
||||
const classes = classNames({
|
||||
|
@ -209,12 +224,12 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
|
|||
'mx_VoiceRecordComposerTile_stop': this.state.recorder?.isRecording,
|
||||
});
|
||||
|
||||
let tooltip = _t("Record a voice message");
|
||||
let tooltip = _t("Send voice message");
|
||||
if (!!this.state.recorder) {
|
||||
tooltip = _t("Stop the recording");
|
||||
tooltip = _t("Stop recording");
|
||||
}
|
||||
|
||||
let stopOrRecordBtn = <AccessibleTooltipButton
|
||||
stopOrRecordBtn = <AccessibleTooltipButton
|
||||
className={classes}
|
||||
onClick={this.onRecordStartEndClick}
|
||||
title={tooltip}
|
||||
|
@ -222,22 +237,41 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
|
|||
if (this.state.recorder && !this.state.recorder?.isRecording) {
|
||||
stopOrRecordBtn = null;
|
||||
}
|
||||
|
||||
recordingInfo = stopOrRecordBtn;
|
||||
}
|
||||
|
||||
if (this.state.recorder && this.state.recordingPhase !== RecordingState.Uploading) {
|
||||
deleteButton = <AccessibleTooltipButton
|
||||
className='mx_VoiceRecordComposerTile_delete'
|
||||
title={_t("Delete recording")}
|
||||
title={_t("Delete")}
|
||||
onClick={this.onCancel}
|
||||
/>;
|
||||
}
|
||||
|
||||
let uploadIndicator;
|
||||
if (this.state.recordingPhase === RecordingState.Uploading) {
|
||||
uploadIndicator = <span className='mx_VoiceRecordComposerTile_uploadingState'>
|
||||
<InlineSpinner w={16} h={16} />
|
||||
</span>;
|
||||
} else if (this.state.didUploadFail && this.state.recordingPhase === RecordingState.Ended) {
|
||||
uploadIndicator = <span className='mx_VoiceRecordComposerTile_failedState'>
|
||||
<span className='mx_VoiceRecordComposerTile_uploadState_badge'>
|
||||
{ /* Need to stick the badge in a span to ensure it doesn't create a block component */ }
|
||||
<NotificationBadge
|
||||
notification={StaticNotificationState.forSymbol("!", NotificationColor.Red)}
|
||||
/>
|
||||
</span>
|
||||
<span className='text-warning'>{ _t("Failed to send") }</span>
|
||||
</span>;
|
||||
}
|
||||
|
||||
// The record button (mic icon) is meant to be on the right edge, but we also want the
|
||||
// stop button to be left of the waveform area. Luckily, none of the surrounding UI is
|
||||
// rendered when we're not recording, so the record button ends up in the correct spot.
|
||||
return (<>
|
||||
{ uploadIndicator }
|
||||
{ deleteButton }
|
||||
{ stopOrRecordBtn }
|
||||
{ this.renderWaveformArea() }
|
||||
{ recordingInfo }
|
||||
</>);
|
||||
}
|
||||
}
|
||||
|
|
133
src/components/views/settings/LayoutSwitcher.tsx
Normal file
133
src/components/views/settings/LayoutSwitcher.tsx
Normal file
|
@ -0,0 +1,133 @@
|
|||
/*
|
||||
Copyright 2019 New Vector Ltd
|
||||
Copyright 2019 - 2021 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import classNames from "classnames";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import EventTilePreview from "../elements/EventTilePreview";
|
||||
import StyledRadioButton from "../elements/StyledRadioButton";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { Layout } from "../../../settings/Layout";
|
||||
import { SettingLevel } from "../../../settings/SettingLevel";
|
||||
|
||||
interface IProps {
|
||||
userId: string;
|
||||
displayName: string;
|
||||
avatarUrl: string;
|
||||
messagePreviewText: string;
|
||||
onLayoutChanged?: (layout: Layout) => void;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
layout: Layout;
|
||||
}
|
||||
|
||||
export default class LayoutSwitcher extends React.Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
layout: SettingsStore.getValue("layout"),
|
||||
};
|
||||
}
|
||||
|
||||
private onLayoutChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
const layout = e.target.value as Layout;
|
||||
|
||||
this.setState({ layout: layout });
|
||||
SettingsStore.setValue("layout", null, SettingLevel.DEVICE, layout);
|
||||
this.props.onLayoutChanged(layout);
|
||||
};
|
||||
|
||||
public render(): JSX.Element {
|
||||
const ircClasses = classNames("mx_LayoutSwitcher_RadioButton", {
|
||||
mx_LayoutSwitcher_RadioButton_selected: this.state.layout == Layout.IRC,
|
||||
});
|
||||
const groupClasses = classNames("mx_LayoutSwitcher_RadioButton", {
|
||||
mx_LayoutSwitcher_RadioButton_selected: this.state.layout == Layout.Group,
|
||||
});
|
||||
const bubbleClasses = classNames("mx_LayoutSwitcher_RadioButton", {
|
||||
mx_LayoutSwitcher_RadioButton_selected: this.state.layout === Layout.Bubble,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="mx_SettingsTab_section mx_LayoutSwitcher">
|
||||
<span className="mx_SettingsTab_subheading">
|
||||
{ _t("Message layout") }
|
||||
</span>
|
||||
|
||||
<div className="mx_LayoutSwitcher_RadioButtons">
|
||||
<label className={ircClasses}>
|
||||
<EventTilePreview
|
||||
className="mx_LayoutSwitcher_RadioButton_preview"
|
||||
message={this.props.messagePreviewText}
|
||||
layout={Layout.IRC}
|
||||
userId={this.props.userId}
|
||||
displayName={this.props.displayName}
|
||||
avatarUrl={this.props.avatarUrl}
|
||||
/>
|
||||
<StyledRadioButton
|
||||
name="layout"
|
||||
value={Layout.IRC}
|
||||
checked={this.state.layout === Layout.IRC}
|
||||
onChange={this.onLayoutChange}
|
||||
>
|
||||
{ _t("IRC") }
|
||||
</StyledRadioButton>
|
||||
</label>
|
||||
<label className={groupClasses}>
|
||||
<EventTilePreview
|
||||
className="mx_LayoutSwitcher_RadioButton_preview"
|
||||
message={this.props.messagePreviewText}
|
||||
layout={Layout.Group}
|
||||
userId={this.props.userId}
|
||||
displayName={this.props.displayName}
|
||||
avatarUrl={this.props.avatarUrl}
|
||||
/>
|
||||
<StyledRadioButton
|
||||
name="layout"
|
||||
value={Layout.Group}
|
||||
checked={this.state.layout == Layout.Group}
|
||||
onChange={this.onLayoutChange}
|
||||
>
|
||||
{ _t("Modern") }
|
||||
</StyledRadioButton>
|
||||
</label>
|
||||
<label className={bubbleClasses}>
|
||||
<EventTilePreview
|
||||
className="mx_LayoutSwitcher_RadioButton_preview"
|
||||
message={this.props.messagePreviewText}
|
||||
layout={Layout.Bubble}
|
||||
userId={this.props.userId}
|
||||
displayName={this.props.displayName}
|
||||
avatarUrl={this.props.avatarUrl}
|
||||
/>
|
||||
<StyledRadioButton
|
||||
name="layout"
|
||||
value={Layout.Bubble}
|
||||
checked={this.state.layout == Layout.Bubble}
|
||||
onChange={this.onLayoutChange}
|
||||
>
|
||||
{ _t("Message bubbles") }
|
||||
</StyledRadioButton>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
Copyright 2019 New Vector Ltd
|
||||
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2019 - 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -37,10 +37,9 @@ import StyledRadioGroup from "../../../elements/StyledRadioGroup";
|
|||
import { SettingLevel } from "../../../../../settings/SettingLevel";
|
||||
import { UIFeature } from "../../../../../settings/UIFeature";
|
||||
import { Layout } from "../../../../../settings/Layout";
|
||||
import classNames from 'classnames';
|
||||
import StyledRadioButton from '../../../elements/StyledRadioButton';
|
||||
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
|
||||
import { compare } from "../../../../../utils/strings";
|
||||
import LayoutSwitcher from "../../LayoutSwitcher";
|
||||
|
||||
interface IProps {
|
||||
}
|
||||
|
@ -243,17 +242,8 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
|
|||
this.setState({ customThemeUrl: e.target.value });
|
||||
};
|
||||
|
||||
private onLayoutChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
let layout;
|
||||
switch (e.target.value) {
|
||||
case "irc": layout = Layout.IRC; break;
|
||||
case "group": layout = Layout.Group; break;
|
||||
case "bubble": layout = Layout.Bubble; break;
|
||||
}
|
||||
|
||||
private onLayoutChanged = (layout: Layout): void => {
|
||||
this.setState({ layout: layout });
|
||||
|
||||
SettingsStore.setValue("layout", null, SettingLevel.DEVICE, layout);
|
||||
};
|
||||
|
||||
private onIRCLayoutChange = (enabled: boolean) => {
|
||||
|
@ -391,75 +381,6 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
|
|||
</div>;
|
||||
}
|
||||
|
||||
private renderLayoutSection = () => {
|
||||
return <div className="mx_SettingsTab_section mx_AppearanceUserSettingsTab_Layout">
|
||||
<span className="mx_SettingsTab_subheading">{ _t("Message layout") }</span>
|
||||
|
||||
<div className="mx_AppearanceUserSettingsTab_Layout_RadioButtons">
|
||||
<label className={classNames("mx_AppearanceUserSettingsTab_Layout_RadioButton", {
|
||||
mx_AppearanceUserSettingsTab_Layout_RadioButton_selected: this.state.layout == Layout.IRC,
|
||||
})}>
|
||||
<EventTilePreview
|
||||
className="mx_AppearanceUserSettingsTab_Layout_RadioButton_preview"
|
||||
message={this.MESSAGE_PREVIEW_TEXT}
|
||||
layout={Layout.IRC}
|
||||
userId={this.state.userId}
|
||||
displayName={this.state.displayName}
|
||||
avatarUrl={this.state.avatarUrl}
|
||||
/>
|
||||
<StyledRadioButton
|
||||
name="layout"
|
||||
value="irc"
|
||||
checked={this.state.layout === Layout.IRC}
|
||||
onChange={this.onLayoutChange}
|
||||
>
|
||||
{ _t("IRC") }
|
||||
</StyledRadioButton>
|
||||
</label>
|
||||
<label className={classNames("mx_AppearanceUserSettingsTab_Layout_RadioButton", {
|
||||
mx_AppearanceUserSettingsTab_Layout_RadioButton_selected: this.state.layout == Layout.Group,
|
||||
})}>
|
||||
<EventTilePreview
|
||||
className="mx_AppearanceUserSettingsTab_Layout_RadioButton_preview"
|
||||
message={this.MESSAGE_PREVIEW_TEXT}
|
||||
layout={Layout.Group}
|
||||
userId={this.state.userId}
|
||||
displayName={this.state.displayName}
|
||||
avatarUrl={this.state.avatarUrl}
|
||||
/>
|
||||
<StyledRadioButton
|
||||
name="layout"
|
||||
value="group"
|
||||
checked={this.state.layout == Layout.Group}
|
||||
onChange={this.onLayoutChange}
|
||||
>
|
||||
{ _t("Modern") }
|
||||
</StyledRadioButton>
|
||||
</label>
|
||||
<label className={classNames("mx_AppearanceUserSettingsTab_Layout_RadioButton", {
|
||||
mx_AppearanceUserSettingsTab_Layout_RadioButton_selected: this.state.layout === Layout.Bubble,
|
||||
})}>
|
||||
<EventTilePreview
|
||||
className="mx_AppearanceUserSettingsTab_Layout_RadioButton_preview"
|
||||
message={this.MESSAGE_PREVIEW_TEXT}
|
||||
layout={Layout.Bubble}
|
||||
userId={this.state.userId}
|
||||
displayName={this.state.displayName}
|
||||
avatarUrl={this.state.avatarUrl}
|
||||
/>
|
||||
<StyledRadioButton
|
||||
name="layout"
|
||||
value="bubble"
|
||||
checked={this.state.layout == Layout.Bubble}
|
||||
onChange={this.onLayoutChange}
|
||||
>
|
||||
{ _t("Message bubbles") }
|
||||
</StyledRadioButton>
|
||||
</label>
|
||||
</div>
|
||||
</div>;
|
||||
};
|
||||
|
||||
private renderAdvancedSection() {
|
||||
if (!SettingsStore.getValue(UIFeature.AdvancedSettings)) return null;
|
||||
|
||||
|
@ -527,6 +448,19 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
|
|||
render() {
|
||||
const brand = SdkConfig.get().brand;
|
||||
|
||||
let layoutSection;
|
||||
if (SettingsStore.getValue("feature_new_layout_switcher")) {
|
||||
layoutSection = (
|
||||
<LayoutSwitcher
|
||||
userId={this.state.userId}
|
||||
displayName={this.state.displayName}
|
||||
avatarUrl={this.state.avatarUrl}
|
||||
messagePreviewText={this.MESSAGE_PREVIEW_TEXT}
|
||||
onLayoutChanged={this.onLayoutChanged}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_SettingsTab mx_AppearanceUserSettingsTab">
|
||||
<div className="mx_SettingsTab_heading">{ _t("Customise your appearance") }</div>
|
||||
|
@ -534,7 +468,7 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
|
|||
{ _t("Appearance Settings only affect this %(brand)s session.", { brand }) }
|
||||
</div>
|
||||
{ this.renderThemeSection() }
|
||||
{ SettingsStore.getValue("feature_new_layout_switcher") ? this.renderLayoutSection() : null }
|
||||
{ layoutSection }
|
||||
{ this.renderFontSection() }
|
||||
{ this.renderAdvancedSection() }
|
||||
</div>
|
||||
|
|
|
@ -15,9 +15,9 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import AccessibleButton, { ButtonEvent } from "../../../elements/AccessibleButton";
|
||||
import { _t, getCurrentLanguage } from "../../../../../languageHandler";
|
||||
import { MatrixClientPeg } from "../../../../../MatrixClientPeg";
|
||||
import AccessibleButton from "../../../elements/AccessibleButton";
|
||||
import AccessibleTooltipButton from '../../../elements/AccessibleTooltipButton';
|
||||
import SdkConfig from "../../../../../SdkConfig";
|
||||
import createRoom from "../../../../../createRoom";
|
||||
|
@ -69,6 +69,18 @@ export default class HelpUserSettingsTab extends React.Component<IProps, IState>
|
|||
if (this.closeCopiedTooltip) this.closeCopiedTooltip();
|
||||
}
|
||||
|
||||
private getVersionInfo(): { appVersion: string, olmVersion: string } {
|
||||
const brand = SdkConfig.get().brand;
|
||||
const appVersion = this.state.appVersion || 'unknown';
|
||||
let olmVersion = MatrixClientPeg.get().olmVersion;
|
||||
olmVersion = olmVersion ? `${olmVersion[0]}.${olmVersion[1]}.${olmVersion[2]}` : '<not-enabled>';
|
||||
|
||||
return {
|
||||
appVersion: `${_t("%(brand)s version:", { brand })} ${appVersion}`,
|
||||
olmVersion: `${_t("Olm version:")} ${olmVersion}`,
|
||||
};
|
||||
}
|
||||
|
||||
private onClearCacheAndReload = (e) => {
|
||||
if (!PlatformPeg.get()) return;
|
||||
|
||||
|
@ -173,17 +185,26 @@ export default class HelpUserSettingsTab extends React.Component<IProps, IState>
|
|||
);
|
||||
}
|
||||
|
||||
onAccessTokenCopyClick = async (e) => {
|
||||
private async copy(text: string, e: ButtonEvent) {
|
||||
e.preventDefault();
|
||||
const target = e.target; // copy target before we go async and React throws it away
|
||||
const target = e.target as HTMLDivElement; // copy target before we go async and React throws it away
|
||||
|
||||
const successful = await copyPlaintext(MatrixClientPeg.get().getAccessToken());
|
||||
const successful = await copyPlaintext(text);
|
||||
const buttonRect = target.getBoundingClientRect();
|
||||
const { close } = ContextMenu.createMenu(GenericTextContextMenu, {
|
||||
...toRightOf(buttonRect, 2),
|
||||
message: successful ? _t('Copied!') : _t('Failed to copy'),
|
||||
});
|
||||
this.closeCopiedTooltip = target.onmouseleave = close;
|
||||
}
|
||||
|
||||
private onAccessTokenCopyClick = (e: ButtonEvent) => {
|
||||
this.copy(MatrixClientPeg.get().getAccessToken(), e);
|
||||
};
|
||||
|
||||
private onCopyVersionClicked = (e: ButtonEvent) => {
|
||||
const { appVersion, olmVersion } = this.getVersionInfo();
|
||||
this.copy(`${appVersion}\n${olmVersion}`, e);
|
||||
};
|
||||
|
||||
render() {
|
||||
|
@ -232,11 +253,6 @@ export default class HelpUserSettingsTab extends React.Component<IProps, IState>
|
|||
);
|
||||
}
|
||||
|
||||
const appVersion = this.state.appVersion || 'unknown';
|
||||
|
||||
let olmVersion = MatrixClientPeg.get().olmVersion;
|
||||
olmVersion = olmVersion ? `${olmVersion[0]}.${olmVersion[1]}.${olmVersion[2]}` : '<not-enabled>';
|
||||
|
||||
let updateButton = null;
|
||||
if (this.state.canUpdate) {
|
||||
updateButton = <UpdateCheckButton />;
|
||||
|
@ -275,6 +291,8 @@ export default class HelpUserSettingsTab extends React.Component<IProps, IState>
|
|||
);
|
||||
}
|
||||
|
||||
const { appVersion, olmVersion } = this.getVersionInfo();
|
||||
|
||||
return (
|
||||
<div className="mx_SettingsTab mx_HelpUserSettingsTab">
|
||||
<div className="mx_SettingsTab_heading">{ _t("Help & About") }</div>
|
||||
|
@ -291,8 +309,15 @@ export default class HelpUserSettingsTab extends React.Component<IProps, IState>
|
|||
<div className='mx_SettingsTab_section mx_HelpUserSettingsTab_versions'>
|
||||
<span className='mx_SettingsTab_subheading'>{ _t("Versions") }</span>
|
||||
<div className='mx_SettingsTab_subsectionText'>
|
||||
{ _t("%(brand)s version:", { brand }) } { appVersion }<br />
|
||||
{ _t("olm version:") } { olmVersion }<br />
|
||||
<div className="mx_HelpUserSettingsTab_copy">
|
||||
{ appVersion }<br />
|
||||
{ olmVersion }<br />
|
||||
<AccessibleTooltipButton
|
||||
title={_t("Copy")}
|
||||
onClick={this.onCopyVersionClicked}
|
||||
className="mx_HelpUserSettingsTab_copyButton"
|
||||
/>
|
||||
</div>
|
||||
{ updateButton }
|
||||
</div>
|
||||
</div>
|
||||
|
@ -308,12 +333,12 @@ export default class HelpUserSettingsTab extends React.Component<IProps, IState>
|
|||
<summary>{ _t("Access Token") }</summary><br />
|
||||
<b>{ _t("Your access token gives full access to your account."
|
||||
+ " Do not share it with anyone." ) }</b>
|
||||
<div className="mx_HelpUserSettingsTab_accessToken">
|
||||
<div className="mx_HelpUserSettingsTab_copy">
|
||||
<code>{ MatrixClientPeg.get().getAccessToken() }</code>
|
||||
<AccessibleTooltipButton
|
||||
title={_t("Copy")}
|
||||
onClick={this.onAccessTokenCopyClick}
|
||||
className="mx_HelpUserSettingsTab_accessToken_copy"
|
||||
className="mx_HelpUserSettingsTab_copyButton"
|
||||
/>
|
||||
</div>
|
||||
</details><br />
|
||||
|
|
|
@ -19,11 +19,12 @@ import { _t } from "../../../../../languageHandler";
|
|||
import PropTypes from "prop-types";
|
||||
import SettingsStore from "../../../../../settings/SettingsStore";
|
||||
import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch";
|
||||
import * as sdk from "../../../../../index";
|
||||
import { SettingLevel } from "../../../../../settings/SettingLevel";
|
||||
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
|
||||
import SdkConfig from "../../../../../SdkConfig";
|
||||
import BetaCard from "../../../beta/BetaCard";
|
||||
import SettingsFlag from '../../../elements/SettingsFlag';
|
||||
import { MatrixClientPeg } from '../../../../../MatrixClientPeg';
|
||||
|
||||
export class LabsSettingToggle extends React.Component {
|
||||
static propTypes = {
|
||||
|
@ -47,6 +48,14 @@ export class LabsSettingToggle extends React.Component {
|
|||
export default class LabsUserSettingsTab extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
MatrixClientPeg.get().doesServerSupportUnstableFeature("org.matrix.msc2285").then((showHiddenReadReceipts) => {
|
||||
this.setState({ showHiddenReadReceipts });
|
||||
});
|
||||
|
||||
this.state = {
|
||||
showHiddenReadReceipts: false,
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
|
@ -65,15 +74,22 @@ export default class LabsUserSettingsTab extends React.Component {
|
|||
|
||||
let labsSection;
|
||||
if (SdkConfig.get()['showLabsSettings']) {
|
||||
const SettingsFlag = sdk.getComponent("views.elements.SettingsFlag");
|
||||
const flags = labs.map(f => <LabsSettingToggle featureId={f} key={f} />);
|
||||
|
||||
let hiddenReadReceipts;
|
||||
if (this.state.showHiddenReadReceipts) {
|
||||
hiddenReadReceipts = (
|
||||
<SettingsFlag name="feature_hidden_read_receipts" level={SettingLevel.ACCOUNT} />
|
||||
);
|
||||
}
|
||||
|
||||
labsSection = <div className="mx_SettingsTab_section">
|
||||
{ flags }
|
||||
<SettingsFlag name="enableWidgetScreenshots" level={SettingLevel.ACCOUNT} />
|
||||
<SettingsFlag name="showHiddenEventsInTimeline" level={SettingLevel.DEVICE} />
|
||||
<SettingsFlag name="lowBandwidth" level={SettingLevel.DEVICE} />
|
||||
<SettingsFlag name="advancedRoomListLogging" level={SettingLevel.DEVICE} />
|
||||
{ hiddenReadReceipts }
|
||||
</div>;
|
||||
}
|
||||
|
||||
|
|
|
@ -61,6 +61,7 @@ export default class PreferencesUserSettingsTab extends React.Component<{}, ISta
|
|||
'MessageComposerInput.suggestEmoji',
|
||||
'sendTypingNotifications',
|
||||
'MessageComposerInput.ctrlEnterToSend',
|
||||
'MessageComposerInput.surroundWith',
|
||||
'MessageComposerInput.showStickersButton',
|
||||
];
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -15,7 +16,6 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import IncomingCallBox from './IncomingCallBox';
|
||||
import CallPreview from './CallPreview';
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
|
||||
|
@ -31,7 +31,6 @@ interface IState {
|
|||
export default class CallContainer extends React.PureComponent<IProps, IState> {
|
||||
public render() {
|
||||
return <div className="mx_CallContainer">
|
||||
<IncomingCallBox />
|
||||
<CallPreview />
|
||||
</div>;
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { createRef } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
import CallView from "./CallView";
|
||||
import RoomViewStore from '../../../stores/RoomViewStore';
|
||||
|
@ -27,23 +27,8 @@ import SettingsStore from "../../../settings/SettingsStore";
|
|||
import { CallEvent, CallState, MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
|
||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import UIStore from '../../../stores/UIStore';
|
||||
import { lerp } from '../../../utils/AnimationUtils';
|
||||
import { MarkedExecution } from '../../../utils/MarkedExecution';
|
||||
import { EventSubscription } from 'fbemitter';
|
||||
|
||||
const PIP_VIEW_WIDTH = 336;
|
||||
const PIP_VIEW_HEIGHT = 232;
|
||||
|
||||
const MOVING_AMT = 0.2;
|
||||
const SNAPPING_AMT = 0.05;
|
||||
|
||||
const PADDING = {
|
||||
top: 58,
|
||||
bottom: 58,
|
||||
left: 76,
|
||||
right: 8,
|
||||
};
|
||||
import PictureInPictureDragger from './PictureInPictureDragger';
|
||||
|
||||
const SHOW_CALL_IN_STATES = [
|
||||
CallState.Connected,
|
||||
|
@ -66,10 +51,6 @@ interface IState {
|
|||
// Any other call we're displaying: only if the user is on two calls and not viewing either of the rooms
|
||||
// they belong to
|
||||
secondaryCall: MatrixCall;
|
||||
|
||||
// Position of the CallPreview
|
||||
translationX: number;
|
||||
translationY: number;
|
||||
}
|
||||
|
||||
// Splits a list of calls into one 'primary' one and a list
|
||||
|
@ -112,16 +93,6 @@ export default class CallPreview extends React.Component<IProps, IState> {
|
|||
private roomStoreToken: EventSubscription;
|
||||
private dispatcherRef: string;
|
||||
private settingsWatcherRef: string;
|
||||
private callViewWrapper = createRef<HTMLDivElement>();
|
||||
private initX = 0;
|
||||
private initY = 0;
|
||||
private desiredTranslationX = UIStore.instance.windowWidth - PADDING.right - PIP_VIEW_WIDTH;
|
||||
private desiredTranslationY = UIStore.instance.windowHeight - PADDING.bottom - PIP_VIEW_WIDTH;
|
||||
private moving = false;
|
||||
private scheduledUpdate = new MarkedExecution(
|
||||
() => this.animationCallback(),
|
||||
() => requestAnimationFrame(() => this.scheduledUpdate.trigger()),
|
||||
);
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
@ -136,17 +107,12 @@ export default class CallPreview extends React.Component<IProps, IState> {
|
|||
roomId,
|
||||
primaryCall: primaryCall,
|
||||
secondaryCall: secondaryCalls[0],
|
||||
translationX: UIStore.instance.windowWidth - PADDING.right - PIP_VIEW_WIDTH,
|
||||
translationY: UIStore.instance.windowHeight - PADDING.bottom - PIP_VIEW_WIDTH,
|
||||
};
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
CallHandler.sharedInstance().addListener(CallHandlerEvent.CallChangeRoom, this.updateCalls);
|
||||
this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate);
|
||||
document.addEventListener("mousemove", this.onMoving);
|
||||
document.addEventListener("mouseup", this.onEndMoving);
|
||||
window.addEventListener("resize", this.onResize);
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
MatrixClientPeg.get().on(CallEvent.RemoteHoldUnhold, this.onCallRemoteHold);
|
||||
}
|
||||
|
@ -154,9 +120,6 @@ export default class CallPreview extends React.Component<IProps, IState> {
|
|||
public componentWillUnmount() {
|
||||
CallHandler.sharedInstance().removeListener(CallHandlerEvent.CallChangeRoom, this.updateCalls);
|
||||
MatrixClientPeg.get().removeListener(CallEvent.RemoteHoldUnhold, this.onCallRemoteHold);
|
||||
document.removeEventListener("mousemove", this.onMoving);
|
||||
document.removeEventListener("mouseup", this.onEndMoving);
|
||||
window.removeEventListener("resize", this.onResize);
|
||||
if (this.roomStoreToken) {
|
||||
this.roomStoreToken.remove();
|
||||
}
|
||||
|
@ -164,94 +127,6 @@ export default class CallPreview extends React.Component<IProps, IState> {
|
|||
SettingsStore.unwatchSetting(this.settingsWatcherRef);
|
||||
}
|
||||
|
||||
private onResize = (): void => {
|
||||
this.snap(false);
|
||||
};
|
||||
|
||||
private animationCallback = () => {
|
||||
// If the PiP isn't being dragged and there is only a tiny difference in
|
||||
// the desiredTranslation and translation, quit the animationCallback
|
||||
// loop. If that is the case, it means the PiP has snapped into its
|
||||
// position and there is nothing to do. Not doing this would cause an
|
||||
// infinite loop
|
||||
if (
|
||||
!this.moving &&
|
||||
Math.abs(this.state.translationX - this.desiredTranslationX) <= 1 &&
|
||||
Math.abs(this.state.translationY - this.desiredTranslationY) <= 1
|
||||
) return;
|
||||
|
||||
const amt = this.moving ? MOVING_AMT : SNAPPING_AMT;
|
||||
this.setState({
|
||||
translationX: lerp(this.state.translationX, this.desiredTranslationX, amt),
|
||||
translationY: lerp(this.state.translationY, this.desiredTranslationY, amt),
|
||||
});
|
||||
this.scheduledUpdate.mark();
|
||||
};
|
||||
|
||||
private setTranslation(inTranslationX: number, inTranslationY: number) {
|
||||
const width = this.callViewWrapper.current?.clientWidth || PIP_VIEW_WIDTH;
|
||||
const height = this.callViewWrapper.current?.clientHeight || PIP_VIEW_HEIGHT;
|
||||
|
||||
// Avoid overflow on the x axis
|
||||
if (inTranslationX + width >= UIStore.instance.windowWidth) {
|
||||
this.desiredTranslationX = UIStore.instance.windowWidth - width;
|
||||
} else if (inTranslationX <= 0) {
|
||||
this.desiredTranslationX = 0;
|
||||
} else {
|
||||
this.desiredTranslationX = inTranslationX;
|
||||
}
|
||||
|
||||
// Avoid overflow on the y axis
|
||||
if (inTranslationY + height >= UIStore.instance.windowHeight) {
|
||||
this.desiredTranslationY = UIStore.instance.windowHeight - height;
|
||||
} else if (inTranslationY <= 0) {
|
||||
this.desiredTranslationY = 0;
|
||||
} else {
|
||||
this.desiredTranslationY = inTranslationY;
|
||||
}
|
||||
}
|
||||
|
||||
private snap(animate?: boolean): void {
|
||||
const translationX = this.desiredTranslationX;
|
||||
const translationY = this.desiredTranslationY;
|
||||
// We subtract the PiP size from the window size in order to calculate
|
||||
// the position to snap to from the PiP center and not its top-left
|
||||
// corner
|
||||
const windowWidth = (
|
||||
UIStore.instance.windowWidth -
|
||||
(this.callViewWrapper.current?.clientWidth || PIP_VIEW_WIDTH)
|
||||
);
|
||||
const windowHeight = (
|
||||
UIStore.instance.windowHeight -
|
||||
(this.callViewWrapper.current?.clientHeight || PIP_VIEW_HEIGHT)
|
||||
);
|
||||
|
||||
if (translationX >= windowWidth / 2 && translationY >= windowHeight / 2) {
|
||||
this.desiredTranslationX = windowWidth - PADDING.right;
|
||||
this.desiredTranslationY = windowHeight - PADDING.bottom;
|
||||
} else if (translationX >= windowWidth / 2 && translationY <= windowHeight / 2) {
|
||||
this.desiredTranslationX = windowWidth - PADDING.right;
|
||||
this.desiredTranslationY = PADDING.top;
|
||||
} else if (translationX <= windowWidth / 2 && translationY >= windowHeight / 2) {
|
||||
this.desiredTranslationX = PADDING.left;
|
||||
this.desiredTranslationY = windowHeight - PADDING.bottom;
|
||||
} else {
|
||||
this.desiredTranslationX = PADDING.left;
|
||||
this.desiredTranslationY = PADDING.top;
|
||||
}
|
||||
|
||||
if (animate) {
|
||||
// We start animating here because we want the PiP to move when we're
|
||||
// resizing the window
|
||||
this.scheduledUpdate.mark();
|
||||
} else {
|
||||
this.setState({
|
||||
translationX: this.desiredTranslationX,
|
||||
translationY: this.desiredTranslationY,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private onRoomViewStoreUpdate = () => {
|
||||
if (RoomViewStore.getRoomId() === this.state.roomId) return;
|
||||
|
||||
|
@ -269,9 +144,10 @@ export default class CallPreview extends React.Component<IProps, IState> {
|
|||
|
||||
private onAction = (payload: ActionPayload) => {
|
||||
switch (payload.action) {
|
||||
// listen for call state changes to prod the render method, which
|
||||
// may hide the global CallView if the call it is tracking is dead
|
||||
case 'call_state': {
|
||||
// listen for call state changes to prod the render method, which
|
||||
// may hide the global CallView if the call it is tracking is dead
|
||||
|
||||
this.updateCalls();
|
||||
break;
|
||||
}
|
||||
|
@ -300,57 +176,26 @@ export default class CallPreview extends React.Component<IProps, IState> {
|
|||
});
|
||||
};
|
||||
|
||||
private onStartMoving = (event: React.MouseEvent) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
this.moving = true;
|
||||
this.initX = event.pageX - this.desiredTranslationX;
|
||||
this.initY = event.pageY - this.desiredTranslationY;
|
||||
this.scheduledUpdate.mark();
|
||||
};
|
||||
|
||||
private onMoving = (event: React.MouseEvent | MouseEvent) => {
|
||||
if (!this.moving) return;
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
this.setTranslation(event.pageX - this.initX, event.pageY - this.initY);
|
||||
};
|
||||
|
||||
private onEndMoving = () => {
|
||||
this.moving = false;
|
||||
this.snap(true);
|
||||
};
|
||||
|
||||
public render() {
|
||||
const pipMode = true;
|
||||
if (this.state.primaryCall) {
|
||||
const translatePixelsX = this.state.translationX + "px";
|
||||
const translatePixelsY = this.state.translationY + "px";
|
||||
const style = {
|
||||
transform: `translateX(${translatePixelsX})
|
||||
translateY(${translatePixelsY})`,
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
<PictureInPictureDragger
|
||||
className="mx_CallPreview"
|
||||
style={style}
|
||||
ref={this.callViewWrapper}
|
||||
draggable={pipMode}
|
||||
>
|
||||
<CallView
|
||||
{ ({ onStartMoving, onResize }) => <CallView
|
||||
onMouseDownOnHeader={onStartMoving}
|
||||
call={this.state.primaryCall}
|
||||
secondaryCall={this.state.secondaryCall}
|
||||
pipMode={true}
|
||||
onMouseDownOnHeader={this.onStartMoving}
|
||||
onResize={this.onResize}
|
||||
/>
|
||||
</div>
|
||||
pipMode={pipMode}
|
||||
onResize={onResize}
|
||||
/> }
|
||||
</PictureInPictureDragger>
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
return <PersistentApp />;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -23,11 +23,16 @@ import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
|||
import { _t, _td } from '../../../languageHandler';
|
||||
import VideoFeed from './VideoFeed';
|
||||
import RoomAvatar from "../avatars/RoomAvatar";
|
||||
import { CallState, CallType, MatrixCall, CallEvent } from 'matrix-js-sdk/src/webrtc/call';
|
||||
import { CallEvent, CallState, CallType, MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
|
||||
import classNames from 'classnames';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import { isOnlyCtrlOrCmdKeyEvent, Key } from '../../../Keyboard';
|
||||
import { alwaysAboveLeftOf, alwaysAboveRightOf, ChevronFace, ContextMenuButton } from '../../structures/ContextMenu';
|
||||
import {
|
||||
alwaysAboveLeftOf,
|
||||
alwaysAboveRightOf,
|
||||
ChevronFace,
|
||||
ContextMenuTooltipButton,
|
||||
} from '../../structures/ContextMenu';
|
||||
import CallContextMenu from '../context_menus/CallContextMenu';
|
||||
import { avatarUrlForMember } from '../../../Avatar';
|
||||
import DialpadContextMenu from '../context_menus/DialpadContextMenu';
|
||||
|
@ -37,26 +42,29 @@ import DesktopCapturerSourcePicker from "../elements/DesktopCapturerSourcePicker
|
|||
import Modal from '../../../Modal';
|
||||
import { SDPStreamMetadataPurpose } from 'matrix-js-sdk/src/webrtc/callEventTypes';
|
||||
import CallViewSidebar from './CallViewSidebar';
|
||||
import CallViewHeader from './CallView/CallViewHeader';
|
||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||
import { Alignment } from "../elements/Tooltip";
|
||||
|
||||
interface IProps {
|
||||
// The call for us to display
|
||||
call: MatrixCall;
|
||||
// The call for us to display
|
||||
call: MatrixCall;
|
||||
|
||||
// Another ongoing call to display information about
|
||||
secondaryCall?: MatrixCall;
|
||||
// Another ongoing call to display information about
|
||||
secondaryCall?: MatrixCall;
|
||||
|
||||
// a callback which is called when the content in the CallView changes
|
||||
// in a way that is likely to cause a resize.
|
||||
onResize?: any;
|
||||
// a callback which is called when the content in the CallView changes
|
||||
// in a way that is likely to cause a resize.
|
||||
onResize?: (event: Event) => void;
|
||||
|
||||
// Whether this call view is for picture-in-picture mode
|
||||
// otherwise, it's the larger call view when viewing the room the call is in.
|
||||
// This is sort of a proxy for a number of things but we currently have no
|
||||
// need to control those things separately, so this is simpler.
|
||||
pipMode?: boolean;
|
||||
// Whether this call view is for picture-in-picture mode
|
||||
// otherwise, it's the larger call view when viewing the room the call is in.
|
||||
// This is sort of a proxy for a number of things but we currently have no
|
||||
// need to control those things separately, so this is simpler.
|
||||
pipMode?: boolean;
|
||||
|
||||
// Used for dragging the PiP CallView
|
||||
onMouseDownOnHeader?: (event: React.MouseEvent) => void;
|
||||
// Used for dragging the PiP CallView
|
||||
onMouseDownOnHeader?: (event: React.MouseEvent<Element, MouseEvent>) => void;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
|
@ -75,6 +83,8 @@ interface IState {
|
|||
sidebarShown: boolean;
|
||||
}
|
||||
|
||||
const tooltipYOffset = -24;
|
||||
|
||||
function getFullScreenElement() {
|
||||
return (
|
||||
document.fullscreenElement ||
|
||||
|
@ -115,7 +125,6 @@ export default class CallView extends React.Component<IProps, IState> {
|
|||
private controlsHideTimer: number = null;
|
||||
private dialpadButton = createRef<HTMLDivElement>();
|
||||
private contextMenuButton = createRef<HTMLDivElement>();
|
||||
private contextMenu = createRef<HTMLDivElement>();
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
@ -144,6 +153,7 @@ export default class CallView extends React.Component<IProps, IState> {
|
|||
public componentDidMount() {
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
document.addEventListener('keydown', this.onNativeKeyDown);
|
||||
this.showControls();
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
|
@ -231,21 +241,6 @@ export default class CallView extends React.Component<IProps, IState> {
|
|||
});
|
||||
};
|
||||
|
||||
private onFullscreenClick = () => {
|
||||
dis.dispatch({
|
||||
action: 'video_fullscreen',
|
||||
fullscreen: true,
|
||||
});
|
||||
};
|
||||
|
||||
private onExpandClick = () => {
|
||||
const userFacingRoomId = CallHandler.sharedInstance().roomIdForCall(this.props.call);
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: userFacingRoomId,
|
||||
});
|
||||
};
|
||||
|
||||
private onControlsHideTimer = () => {
|
||||
if (this.state.hoveringControls || this.state.showDialpad || this.state.showMoreMenu) return;
|
||||
this.controlsHideTimer = null;
|
||||
|
@ -389,23 +384,6 @@ export default class CallView extends React.Component<IProps, IState> {
|
|||
this.setState({ hoveringControls: false });
|
||||
};
|
||||
|
||||
private onRoomAvatarClick = (): void => {
|
||||
const userFacingRoomId = CallHandler.sharedInstance().roomIdForCall(this.props.call);
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: userFacingRoomId,
|
||||
});
|
||||
};
|
||||
|
||||
private onSecondaryRoomAvatarClick = (): void => {
|
||||
const userFacingRoomId = CallHandler.sharedInstance().roomIdForCall(this.props.secondaryCall);
|
||||
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: userFacingRoomId,
|
||||
});
|
||||
};
|
||||
|
||||
private onCallResumeClick = (): void => {
|
||||
const userFacingRoomId = CallHandler.sharedInstance().roomIdForCall(this.props.call);
|
||||
CallHandler.sharedInstance().setActiveCallRoomId(userFacingRoomId);
|
||||
|
@ -479,9 +457,12 @@ export default class CallView extends React.Component<IProps, IState> {
|
|||
let vidMuteButton;
|
||||
if (this.props.call.type === CallType.Video) {
|
||||
vidMuteButton = (
|
||||
<AccessibleButton
|
||||
<AccessibleTooltipButton
|
||||
className={vidClasses}
|
||||
onClick={this.onVidMuteClick}
|
||||
title={this.state.vidMuted ? _t("Start the camera") : _t("Stop the camera")}
|
||||
alignment={Alignment.Top}
|
||||
yOffset={tooltipYOffset}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -496,9 +477,15 @@ export default class CallView extends React.Component<IProps, IState> {
|
|||
this.props.call.state === CallState.Connected
|
||||
) {
|
||||
screensharingButton = (
|
||||
<AccessibleButton
|
||||
<AccessibleTooltipButton
|
||||
className={screensharingClasses}
|
||||
onClick={this.onScreenshareClick}
|
||||
title={this.state.screensharing
|
||||
? _t("Stop sharing your screen")
|
||||
: _t("Start sharing your screen")
|
||||
}
|
||||
alignment={Alignment.Top}
|
||||
yOffset={tooltipYOffset}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -518,6 +505,7 @@ export default class CallView extends React.Component<IProps, IState> {
|
|||
<AccessibleButton
|
||||
className={sidebarButtonClasses}
|
||||
onClick={this.onToggleSidebar}
|
||||
aria-label={this.state.sidebarShown ? _t("Hide sidebar") : _t("Show sidebar")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -526,22 +514,28 @@ export default class CallView extends React.Component<IProps, IState> {
|
|||
let contextMenuButton;
|
||||
if (this.state.callState === CallState.Connected) {
|
||||
contextMenuButton = (
|
||||
<ContextMenuButton
|
||||
<ContextMenuTooltipButton
|
||||
className="mx_CallView_callControls_button mx_CallView_callControls_button_more"
|
||||
onClick={this.onMoreClick}
|
||||
inputRef={this.contextMenuButton}
|
||||
isExpanded={this.state.showMoreMenu}
|
||||
title={_t("More")}
|
||||
alignment={Alignment.Top}
|
||||
yOffset={tooltipYOffset}
|
||||
/>
|
||||
);
|
||||
}
|
||||
let dialpadButton;
|
||||
if (this.state.callState === CallState.Connected && this.props.call.opponentSupportsDTMF()) {
|
||||
dialpadButton = (
|
||||
<ContextMenuButton
|
||||
<ContextMenuTooltipButton
|
||||
className="mx_CallView_callControls_button mx_CallView_callControls_dialpad"
|
||||
inputRef={this.dialpadButton}
|
||||
onClick={this.onDialpadClick}
|
||||
isExpanded={this.state.showDialpad}
|
||||
title={_t("Dialpad")}
|
||||
alignment={Alignment.Top}
|
||||
yOffset={tooltipYOffset}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -554,7 +548,11 @@ export default class CallView extends React.Component<IProps, IState> {
|
|||
ChevronFace.None,
|
||||
CONTEXT_MENU_VPADDING,
|
||||
)}
|
||||
mountAsChild={true}
|
||||
// We mount the context menus as a as a child typically in order to include the
|
||||
// context menus when fullscreening the call content.
|
||||
// However, this does not work as well when the call is embedded in a
|
||||
// picture-in-picture frame. Thus, only mount as child when we are *not* in PiP.
|
||||
mountAsChild={!this.props.pipMode}
|
||||
onFinished={this.closeDialpad}
|
||||
call={this.props.call}
|
||||
/>;
|
||||
|
@ -568,7 +566,7 @@ export default class CallView extends React.Component<IProps, IState> {
|
|||
ChevronFace.None,
|
||||
CONTEXT_MENU_VPADDING,
|
||||
)}
|
||||
mountAsChild={true}
|
||||
mountAsChild={!this.props.pipMode}
|
||||
onFinished={this.closeContextMenu}
|
||||
call={this.props.call}
|
||||
/>;
|
||||
|
@ -583,9 +581,12 @@ export default class CallView extends React.Component<IProps, IState> {
|
|||
{ dialPad }
|
||||
{ contextMenu }
|
||||
{ dialpadButton }
|
||||
<AccessibleButton
|
||||
<AccessibleTooltipButton
|
||||
className={micClasses}
|
||||
onClick={this.onMicMuteClick}
|
||||
title={this.state.micMuted ? _t("Unmute the microphone") : _t("Mute the microphone")}
|
||||
alignment={Alignment.Top}
|
||||
yOffset={tooltipYOffset}
|
||||
/>
|
||||
{ vidMuteButton }
|
||||
<div className={micCacheClasses} />
|
||||
|
@ -593,9 +594,12 @@ export default class CallView extends React.Component<IProps, IState> {
|
|||
{ screensharingButton }
|
||||
{ sidebarButton }
|
||||
{ contextMenuButton }
|
||||
<AccessibleButton
|
||||
<AccessibleTooltipButton
|
||||
className="mx_CallView_callControls_button mx_CallView_callControls_button_hangup"
|
||||
onClick={this.onHangupClick}
|
||||
title={_t("Hangup")}
|
||||
alignment={Alignment.Top}
|
||||
yOffset={tooltipYOffset}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -692,7 +696,7 @@ export default class CallView extends React.Component<IProps, IState> {
|
|||
let onHoldBackground = null;
|
||||
const backgroundStyle: CSSProperties = {};
|
||||
const backgroundAvatarUrl = avatarUrlForMember(
|
||||
// is it worth getting the size of the div to pass here?
|
||||
// is it worth getting the size of the div to pass here?
|
||||
this.props.call.getOpponentMember(), 1024, 1024, 'crop',
|
||||
);
|
||||
backgroundStyle.backgroundImage = 'url(' + backgroundAvatarUrl + ')';
|
||||
|
@ -712,7 +716,7 @@ export default class CallView extends React.Component<IProps, IState> {
|
|||
mx_CallView_voice_hold: isOnHold,
|
||||
});
|
||||
|
||||
contentView =(
|
||||
contentView = (
|
||||
<div className={classes} onMouseMove={this.onMouseMove}>
|
||||
<div className="mx_CallView_voice_avatarsContainer">
|
||||
<div
|
||||
|
@ -814,83 +818,15 @@ export default class CallView extends React.Component<IProps, IState> {
|
|||
);
|
||||
}
|
||||
|
||||
const callTypeText = isVideoCall ? _t("Video Call") : _t("Voice Call");
|
||||
let myClassName;
|
||||
|
||||
let fullScreenButton;
|
||||
if (!this.props.pipMode) {
|
||||
fullScreenButton = (
|
||||
<div
|
||||
className="mx_CallView_header_button mx_CallView_header_button_fullscreen"
|
||||
onClick={this.onFullscreenClick}
|
||||
title={_t("Fill Screen")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
let expandButton;
|
||||
if (this.props.pipMode) {
|
||||
expandButton = <div
|
||||
className="mx_CallView_header_button mx_CallView_header_button_expand"
|
||||
onClick={this.onExpandClick}
|
||||
title={_t("Return to call")}
|
||||
/>;
|
||||
}
|
||||
|
||||
const headerControls = <div className="mx_CallView_header_controls">
|
||||
{ fullScreenButton }
|
||||
{ expandButton }
|
||||
</div>;
|
||||
|
||||
const callTypeIconClassName = classNames("mx_CallView_header_callTypeIcon", {
|
||||
"mx_CallView_header_callTypeIcon_voice": !isVideoCall,
|
||||
"mx_CallView_header_callTypeIcon_video": isVideoCall,
|
||||
});
|
||||
|
||||
let header: React.ReactNode;
|
||||
if (!this.props.pipMode) {
|
||||
header = <div className="mx_CallView_header">
|
||||
<div className={callTypeIconClassName} />
|
||||
<span className="mx_CallView_header_callType">{ callTypeText }</span>
|
||||
{ headerControls }
|
||||
</div>;
|
||||
myClassName = 'mx_CallView_large';
|
||||
} else {
|
||||
let secondaryCallInfo;
|
||||
if (this.props.secondaryCall) {
|
||||
secondaryCallInfo = <span className="mx_CallView_header_secondaryCallInfo">
|
||||
<AccessibleButton element='span' onClick={this.onSecondaryRoomAvatarClick}>
|
||||
<RoomAvatar room={secCallRoom} height={16} width={16} />
|
||||
<span className="mx_CallView_secondaryCall_roomName">
|
||||
{ _t("%(name)s on hold", { name: secCallRoom.name }) }
|
||||
</span>
|
||||
</AccessibleButton>
|
||||
</span>;
|
||||
}
|
||||
|
||||
header = (
|
||||
<div
|
||||
className="mx_CallView_header"
|
||||
onMouseDown={this.props.onMouseDownOnHeader}
|
||||
>
|
||||
<AccessibleButton onClick={this.onRoomAvatarClick}>
|
||||
<RoomAvatar room={callRoom} height={32} width={32} />
|
||||
</AccessibleButton>
|
||||
<div className="mx_CallView_header_callInfo">
|
||||
<div className="mx_CallView_header_roomName">{ callRoom.name }</div>
|
||||
<div className="mx_CallView_header_callTypeSmall">
|
||||
{ callTypeText }
|
||||
{ secondaryCallInfo }
|
||||
</div>
|
||||
</div>
|
||||
{ headerControls }
|
||||
</div>
|
||||
);
|
||||
myClassName = 'mx_CallView_pip';
|
||||
}
|
||||
const myClassName = this.props.pipMode ? 'mx_CallView_pip' : 'mx_CallView_large';
|
||||
|
||||
return <div className={"mx_CallView " + myClassName}>
|
||||
{ header }
|
||||
<CallViewHeader
|
||||
onPipMouseDown={this.props.onMouseDownOnHeader}
|
||||
pipMode={this.props.pipMode}
|
||||
type={this.props.call.type}
|
||||
callRooms={[callRoom, secCallRoom]}
|
||||
/>
|
||||
{ contentView }
|
||||
</div>;
|
||||
}
|
||||
|
|
135
src/components/views/voip/CallView/CallViewHeader.tsx
Normal file
135
src/components/views/voip/CallView/CallViewHeader.tsx
Normal file
|
@ -0,0 +1,135 @@
|
|||
/*
|
||||
Copyright 2021 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { CallType } from 'matrix-js-sdk/src/webrtc/call';
|
||||
import { Room } from 'matrix-js-sdk/src/models/room';
|
||||
import React from 'react';
|
||||
import { _t, _td } from '../../../../languageHandler';
|
||||
import RoomAvatar from '../../avatars/RoomAvatar';
|
||||
import AccessibleButton from '../../elements/AccessibleButton';
|
||||
import dis from '../../../../dispatcher/dispatcher';
|
||||
import classNames from 'classnames';
|
||||
import AccessibleTooltipButton from '../../elements/AccessibleTooltipButton';
|
||||
|
||||
const callTypeTranslationByType: Record<CallType, string> = {
|
||||
[CallType.Video]: _td("Video Call"),
|
||||
[CallType.Voice]: _td("Voice Call"),
|
||||
};
|
||||
|
||||
interface CallViewHeaderProps {
|
||||
pipMode: boolean;
|
||||
type: CallType;
|
||||
callRooms?: Room[];
|
||||
onPipMouseDown: (event: React.MouseEvent<Element, MouseEvent>) => void;
|
||||
}
|
||||
|
||||
const onRoomAvatarClick = (roomId: string) => {
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: roomId,
|
||||
});
|
||||
};
|
||||
|
||||
const onFullscreenClick = () => {
|
||||
dis.dispatch({
|
||||
action: 'video_fullscreen',
|
||||
fullscreen: true,
|
||||
});
|
||||
};
|
||||
|
||||
const onExpandClick = (roomId: string) => {
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: roomId,
|
||||
});
|
||||
};
|
||||
|
||||
type CallControlsProps = Pick<CallViewHeaderProps, 'pipMode' | 'type'> & {
|
||||
roomId: string;
|
||||
};
|
||||
const CallViewHeaderControls: React.FC<CallControlsProps> = ({ pipMode = false, type, roomId }) => {
|
||||
return <div className="mx_CallViewHeader_controls">
|
||||
{ !pipMode && <AccessibleTooltipButton
|
||||
className="mx_CallViewHeader_button mx_CallViewHeader_button_fullscreen"
|
||||
onClick={onFullscreenClick}
|
||||
title={_t("Fill Screen")}
|
||||
/> }
|
||||
{ pipMode && <AccessibleTooltipButton
|
||||
className="mx_CallViewHeader_button mx_CallViewHeader_button_expand"
|
||||
onClick={() => onExpandClick(roomId)}
|
||||
title={_t("Return to call")}
|
||||
/> }
|
||||
</div>;
|
||||
};
|
||||
const SecondaryCallInfo: React.FC<{ callRoom: Room }> = ({ callRoom }) => {
|
||||
return <span className="mx_CallViewHeader_secondaryCallInfo">
|
||||
<AccessibleButton element='span' onClick={() => onRoomAvatarClick(callRoom.roomId)}>
|
||||
<RoomAvatar room={callRoom} height={16} width={16} />
|
||||
<span className="mx_CallView_secondaryCall_roomName">
|
||||
{ _t("%(name)s on hold", { name: callRoom.name }) }
|
||||
</span>
|
||||
</AccessibleButton>
|
||||
</span>;
|
||||
};
|
||||
|
||||
const CallTypeIcon: React.FC<{ type: CallType }> = ({ type }) => {
|
||||
const classes = classNames({
|
||||
'mx_CallViewHeader_callTypeIcon': true,
|
||||
'mx_CallViewHeader_callTypeIcon_video': type === CallType.Video,
|
||||
'mx_CallViewHeader_callTypeIcon_voice': type === CallType.Voice,
|
||||
});
|
||||
return <div className={classes} />;
|
||||
};
|
||||
|
||||
const CallViewHeader: React.FC<CallViewHeaderProps> = ({
|
||||
type,
|
||||
pipMode = false,
|
||||
callRooms = [],
|
||||
onPipMouseDown,
|
||||
}) => {
|
||||
const [callRoom, onHoldCallRoom] = callRooms;
|
||||
const callTypeText = _t(callTypeTranslationByType[type]);
|
||||
const callRoomName = callRoom.name;
|
||||
const { roomId } = callRoom;
|
||||
|
||||
if (!pipMode) {
|
||||
return <div className="mx_CallViewHeader">
|
||||
<CallTypeIcon type={type} />
|
||||
<span className="mx_CallViewHeader_callType">{ callTypeText }</span>
|
||||
<CallViewHeaderControls roomId={roomId} pipMode={pipMode} type={type} />
|
||||
</div>;
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className="mx_CallViewHeader"
|
||||
onMouseDown={onPipMouseDown}
|
||||
>
|
||||
<AccessibleButton onClick={() => onRoomAvatarClick(roomId)}>
|
||||
<RoomAvatar room={callRoom} height={32} width={32} />
|
||||
</AccessibleButton>
|
||||
<div className="mx_CallViewHeader_callInfo">
|
||||
<div className="mx_CallViewHeader_roomName">{ callRoomName }</div>
|
||||
<div className="mx_CallViewHeader_callTypeSmall">
|
||||
{ callTypeText }
|
||||
{ onHoldCallRoom && <SecondaryCallInfo callRoom={onHoldCallRoom} /> }
|
||||
</div>
|
||||
</div>
|
||||
<CallViewHeaderControls roomId={roomId} pipMode={pipMode} type={type} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CallViewHeader;
|
|
@ -1,176 +0,0 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { ActionPayload } from '../../../dispatcher/payloads';
|
||||
import CallHandler, { CallHandlerEvent } from '../../../CallHandler';
|
||||
import RoomAvatar from '../avatars/RoomAvatar';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import { CallState } from 'matrix-js-sdk/src/webrtc/call';
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import AccessibleTooltipButton from '../elements/AccessibleTooltipButton';
|
||||
import classNames from 'classnames';
|
||||
|
||||
interface IProps {
|
||||
}
|
||||
|
||||
interface IState {
|
||||
incomingCall: any;
|
||||
silenced: boolean;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.voip.IncomingCallBox")
|
||||
export default class IncomingCallBox extends React.Component<IProps, IState> {
|
||||
private dispatcherRef: string;
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
this.state = {
|
||||
incomingCall: null,
|
||||
silenced: false,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount = () => {
|
||||
CallHandler.sharedInstance().addListener(CallHandlerEvent.SilencedCallsChanged, this.onSilencedCallsChanged);
|
||||
};
|
||||
|
||||
public componentWillUnmount() {
|
||||
dis.unregister(this.dispatcherRef);
|
||||
CallHandler.sharedInstance().removeListener(CallHandlerEvent.SilencedCallsChanged, this.onSilencedCallsChanged);
|
||||
}
|
||||
|
||||
private onAction = (payload: ActionPayload) => {
|
||||
switch (payload.action) {
|
||||
case 'call_state': {
|
||||
const call = CallHandler.sharedInstance().getCallForRoom(payload.room_id);
|
||||
if (call && call.state === CallState.Ringing) {
|
||||
this.setState({
|
||||
incomingCall: call,
|
||||
silenced: false, // Reset silenced state for new call
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
incomingCall: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private onSilencedCallsChanged = () => {
|
||||
const callId = this.state.incomingCall?.callId;
|
||||
if (!callId) return;
|
||||
this.setState({ silenced: CallHandler.sharedInstance().isCallSilenced(callId) });
|
||||
};
|
||||
|
||||
private onAnswerClick: React.MouseEventHandler = (e) => {
|
||||
e.stopPropagation();
|
||||
dis.dispatch({
|
||||
action: 'answer',
|
||||
room_id: CallHandler.sharedInstance().roomIdForCall(this.state.incomingCall),
|
||||
});
|
||||
};
|
||||
|
||||
private onRejectClick: React.MouseEventHandler = (e) => {
|
||||
e.stopPropagation();
|
||||
dis.dispatch({
|
||||
action: 'reject',
|
||||
room_id: CallHandler.sharedInstance().roomIdForCall(this.state.incomingCall),
|
||||
});
|
||||
};
|
||||
|
||||
private onSilenceClick: React.MouseEventHandler = (e) => {
|
||||
e.stopPropagation();
|
||||
const callId = this.state.incomingCall.callId;
|
||||
this.state.silenced ?
|
||||
CallHandler.sharedInstance().unSilenceCall(callId):
|
||||
CallHandler.sharedInstance().silenceCall(callId);
|
||||
};
|
||||
|
||||
public render() {
|
||||
if (!this.state.incomingCall) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let room = null;
|
||||
if (this.state.incomingCall) {
|
||||
room = MatrixClientPeg.get().getRoom(CallHandler.sharedInstance().roomIdForCall(this.state.incomingCall));
|
||||
}
|
||||
|
||||
const caller = room ? room.name : _t("Unknown caller");
|
||||
|
||||
let incomingCallText = null;
|
||||
if (this.state.incomingCall) {
|
||||
if (this.state.incomingCall.type === "voice") {
|
||||
incomingCallText = _t("Incoming voice call");
|
||||
} else if (this.state.incomingCall.type === "video") {
|
||||
incomingCallText = _t("Incoming video call");
|
||||
} else {
|
||||
incomingCallText = _t("Incoming call");
|
||||
}
|
||||
}
|
||||
|
||||
const silenceClass = classNames({
|
||||
"mx_IncomingCallBox_iconButton": true,
|
||||
"mx_IncomingCallBox_unSilence": this.state.silenced,
|
||||
"mx_IncomingCallBox_silence": !this.state.silenced,
|
||||
});
|
||||
|
||||
return <div className="mx_IncomingCallBox">
|
||||
<div className="mx_IncomingCallBox_CallerInfo">
|
||||
<RoomAvatar
|
||||
room={room}
|
||||
height={32}
|
||||
width={32}
|
||||
/>
|
||||
<div>
|
||||
<h1>{ caller }</h1>
|
||||
<p>{ incomingCallText }</p>
|
||||
</div>
|
||||
<AccessibleTooltipButton
|
||||
className={silenceClass}
|
||||
onClick={this.onSilenceClick}
|
||||
title={this.state.silenced ? _t("Sound on"): _t("Silence call")}
|
||||
/>
|
||||
</div>
|
||||
<div className="mx_IncomingCallBox_buttons">
|
||||
<AccessibleButton
|
||||
className="mx_IncomingCallBox_decline"
|
||||
onClick={this.onRejectClick}
|
||||
kind="danger"
|
||||
>
|
||||
{ _t("Decline") }
|
||||
</AccessibleButton>
|
||||
<div className="mx_IncomingCallBox_spacer" />
|
||||
<AccessibleButton
|
||||
className="mx_IncomingCallBox_accept"
|
||||
onClick={this.onAnswerClick}
|
||||
kind="primary"
|
||||
>
|
||||
{ _t("Accept") }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
}
|
229
src/components/views/voip/PictureInPictureDragger.tsx
Normal file
229
src/components/views/voip/PictureInPictureDragger.tsx
Normal file
|
@ -0,0 +1,229 @@
|
|||
/*
|
||||
Copyright 2021 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { createRef } from 'react';
|
||||
import UIStore from '../../../stores/UIStore';
|
||||
import { lerp } from '../../../utils/AnimationUtils';
|
||||
import { MarkedExecution } from '../../../utils/MarkedExecution';
|
||||
import { replaceableComponent } from '../../../utils/replaceableComponent';
|
||||
|
||||
const PIP_VIEW_WIDTH = 336;
|
||||
const PIP_VIEW_HEIGHT = 232;
|
||||
|
||||
const MOVING_AMT = 0.2;
|
||||
const SNAPPING_AMT = 0.1;
|
||||
|
||||
const PADDING = {
|
||||
top: 58,
|
||||
bottom: 58,
|
||||
left: 76,
|
||||
right: 8,
|
||||
};
|
||||
|
||||
interface IChildrenOptions {
|
||||
onStartMoving: (event: React.MouseEvent<Element, MouseEvent>) => void;
|
||||
onResize: (event: Event) => void;
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
className?: string;
|
||||
children: ({ onStartMoving, onResize }: IChildrenOptions) => React.ReactNode;
|
||||
draggable: boolean;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
// Position of the PictureInPictureDragger
|
||||
translationX: number;
|
||||
translationY: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* PictureInPictureDragger shows a small version of CallView hovering over the UI in 'picture-in-picture'
|
||||
* (PiP mode). It displays the call(s) which is *not* in the room the user is currently viewing.
|
||||
*/
|
||||
@replaceableComponent("views.voip.PictureInPictureDragger")
|
||||
export default class PictureInPictureDragger extends React.Component<IProps, IState> {
|
||||
private callViewWrapper = createRef<HTMLDivElement>();
|
||||
private initX = 0;
|
||||
private initY = 0;
|
||||
private desiredTranslationX = UIStore.instance.windowWidth - PADDING.right - PIP_VIEW_WIDTH;
|
||||
private desiredTranslationY = UIStore.instance.windowHeight - PADDING.bottom - PIP_VIEW_HEIGHT;
|
||||
private moving = false;
|
||||
private scheduledUpdate = new MarkedExecution(
|
||||
() => this.animationCallback(),
|
||||
() => requestAnimationFrame(() => this.scheduledUpdate.trigger()),
|
||||
);
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
translationX: UIStore.instance.windowWidth - PADDING.right - PIP_VIEW_WIDTH,
|
||||
translationY: UIStore.instance.windowHeight - PADDING.bottom - PIP_VIEW_HEIGHT,
|
||||
};
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
document.addEventListener("mousemove", this.onMoving);
|
||||
document.addEventListener("mouseup", this.onEndMoving);
|
||||
window.addEventListener("resize", this.onResize);
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
document.removeEventListener("mousemove", this.onMoving);
|
||||
document.removeEventListener("mouseup", this.onEndMoving);
|
||||
window.removeEventListener("resize", this.onResize);
|
||||
}
|
||||
|
||||
private animationCallback = () => {
|
||||
// If the PiP isn't being dragged and there is only a tiny difference in
|
||||
// the desiredTranslation and translation, quit the animationCallback
|
||||
// loop. If that is the case, it means the PiP has snapped into its
|
||||
// position and there is nothing to do. Not doing this would cause an
|
||||
// infinite loop
|
||||
if (
|
||||
!this.moving &&
|
||||
Math.abs(this.state.translationX - this.desiredTranslationX) <= 1 &&
|
||||
Math.abs(this.state.translationY - this.desiredTranslationY) <= 1
|
||||
) return;
|
||||
|
||||
const amt = this.moving ? MOVING_AMT : SNAPPING_AMT;
|
||||
this.setState({
|
||||
translationX: lerp(this.state.translationX, this.desiredTranslationX, amt),
|
||||
translationY: lerp(this.state.translationY, this.desiredTranslationY, amt),
|
||||
});
|
||||
this.scheduledUpdate.mark();
|
||||
};
|
||||
|
||||
private setTranslation(inTranslationX: number, inTranslationY: number) {
|
||||
const width = this.callViewWrapper.current?.clientWidth || PIP_VIEW_WIDTH;
|
||||
const height = this.callViewWrapper.current?.clientHeight || PIP_VIEW_HEIGHT;
|
||||
|
||||
// Avoid overflow on the x axis
|
||||
if (inTranslationX + width >= UIStore.instance.windowWidth) {
|
||||
this.desiredTranslationX = UIStore.instance.windowWidth - width;
|
||||
} else if (inTranslationX <= 0) {
|
||||
this.desiredTranslationX = 0;
|
||||
} else {
|
||||
this.desiredTranslationX = inTranslationX;
|
||||
}
|
||||
|
||||
// Avoid overflow on the y axis
|
||||
if (inTranslationY + height >= UIStore.instance.windowHeight) {
|
||||
this.desiredTranslationY = UIStore.instance.windowHeight - height;
|
||||
} else if (inTranslationY <= 0) {
|
||||
this.desiredTranslationY = 0;
|
||||
} else {
|
||||
this.desiredTranslationY = inTranslationY;
|
||||
}
|
||||
}
|
||||
|
||||
private onResize = (): void => {
|
||||
this.snap(false);
|
||||
};
|
||||
|
||||
private snap = (animate = false) => {
|
||||
const translationX = this.desiredTranslationX;
|
||||
const translationY = this.desiredTranslationY;
|
||||
// We subtract the PiP size from the window size in order to calculate
|
||||
// the position to snap to from the PiP center and not its top-left
|
||||
// corner
|
||||
const windowWidth = (
|
||||
UIStore.instance.windowWidth -
|
||||
(this.callViewWrapper.current?.clientWidth || PIP_VIEW_WIDTH)
|
||||
);
|
||||
const windowHeight = (
|
||||
UIStore.instance.windowHeight -
|
||||
(this.callViewWrapper.current?.clientHeight || PIP_VIEW_HEIGHT)
|
||||
);
|
||||
|
||||
if (translationX >= windowWidth / 2 && translationY >= windowHeight / 2) {
|
||||
this.desiredTranslationX = windowWidth - PADDING.right;
|
||||
this.desiredTranslationY = windowHeight - PADDING.bottom;
|
||||
} else if (translationX >= windowWidth / 2 && translationY <= windowHeight / 2) {
|
||||
this.desiredTranslationX = windowWidth - PADDING.right;
|
||||
this.desiredTranslationY = PADDING.top;
|
||||
} else if (translationX <= windowWidth / 2 && translationY >= windowHeight / 2) {
|
||||
this.desiredTranslationX = PADDING.left;
|
||||
this.desiredTranslationY = windowHeight - PADDING.bottom;
|
||||
} else {
|
||||
this.desiredTranslationX = PADDING.left;
|
||||
this.desiredTranslationY = PADDING.top;
|
||||
}
|
||||
|
||||
// We start animating here because we want the PiP to move when we're
|
||||
// resizing the window
|
||||
this.scheduledUpdate.mark();
|
||||
|
||||
if (animate) {
|
||||
// We start animating here because we want the PiP to move when we're
|
||||
// resizing the window
|
||||
this.scheduledUpdate.mark();
|
||||
} else {
|
||||
this.setState({
|
||||
translationX: this.desiredTranslationX,
|
||||
translationY: this.desiredTranslationY,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private onStartMoving = (event: React.MouseEvent | MouseEvent) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
this.moving = true;
|
||||
this.initX = event.pageX - this.desiredTranslationX;
|
||||
this.initY = event.pageY - this.desiredTranslationY;
|
||||
this.scheduledUpdate.mark();
|
||||
};
|
||||
|
||||
private onMoving = (event: React.MouseEvent | MouseEvent) => {
|
||||
if (!this.moving) return;
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
this.setTranslation(event.pageX - this.initX, event.pageY - this.initY);
|
||||
};
|
||||
|
||||
private onEndMoving = () => {
|
||||
this.moving = false;
|
||||
this.snap(true);
|
||||
};
|
||||
|
||||
public render() {
|
||||
const translatePixelsX = this.state.translationX + "px";
|
||||
const translatePixelsY = this.state.translationY + "px";
|
||||
const style = {
|
||||
transform: `translateX(${translatePixelsX})
|
||||
translateY(${translatePixelsY})`,
|
||||
};
|
||||
return (
|
||||
<div
|
||||
className={this.props.className}
|
||||
style={this.props.draggable ? style : undefined}
|
||||
ref={this.callViewWrapper}
|
||||
>
|
||||
<>
|
||||
{ this.props.children({
|
||||
onStartMoving: this.onStartMoving,
|
||||
onResize: this.onResize,
|
||||
}) }
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -207,6 +207,7 @@ export default class VideoFeed extends React.PureComponent<IProps, IState> {
|
|||
const videoClasses = classnames("mx_VideoFeed_video", {
|
||||
mx_VideoFeed_video_mirror: (
|
||||
this.props.feed.isLocal() &&
|
||||
this.props.feed.purpose === SDPStreamMetadataPurpose.Usermedia &&
|
||||
SettingsStore.getValue('VideoView.flipVideoHorizontally')
|
||||
),
|
||||
});
|
||||
|
|
16
src/emoji.ts
16
src/emoji.ts
|
@ -35,6 +35,16 @@ export const EMOTICON_TO_EMOJI = new Map<string, IEmoji>();
|
|||
|
||||
export const getEmojiFromUnicode = unicode => UNICODE_TO_EMOJI.get(stripVariation(unicode));
|
||||
|
||||
const isRegionalIndicator = (x: string): boolean => {
|
||||
// First verify that the string is a single character. We use Array.from
|
||||
// to make sure we count by characters, not UTF-8 code units.
|
||||
return Array.from(x).length === 1 &&
|
||||
// Next verify that the character is within the code point range for
|
||||
// regional indicators.
|
||||
// http://unicode.org/charts/PDF/Unicode-6.0/U60-1F100.pdf
|
||||
x >= '\u{1f1e6}' && x <= '\u{1f1ff}';
|
||||
};
|
||||
|
||||
const EMOJIBASE_GROUP_ID_TO_CATEGORY = [
|
||||
"people", // smileys
|
||||
"people", // actually people
|
||||
|
@ -72,7 +82,11 @@ export const EMOJI: IEmoji[] = EMOJIBASE.map((emojiData: Omit<IEmoji, "shortcode
|
|||
shortcodes: typeof shortcodeData === "string" ? [shortcodeData] : shortcodeData,
|
||||
};
|
||||
|
||||
const categoryId = EMOJIBASE_GROUP_ID_TO_CATEGORY[emoji.group];
|
||||
// We manually include regional indicators in the symbols group, since
|
||||
// Emojibase intentionally leaves them uncategorized
|
||||
const categoryId = EMOJIBASE_GROUP_ID_TO_CATEGORY[emoji.group] ??
|
||||
(isRegionalIndicator(emoji.unicode) ? "symbols" : null);
|
||||
|
||||
if (DATA_BY_CATEGORY.hasOwnProperty(categoryId)) {
|
||||
DATA_BY_CATEGORY[categoryId].push(emoji);
|
||||
}
|
||||
|
|
|
@ -64,8 +64,6 @@
|
|||
"Unable to transfer call": "Unable to transfer call",
|
||||
"Transfer Failed": "Transfer Failed",
|
||||
"Failed to transfer call": "Failed to transfer call",
|
||||
"Call in Progress": "Call in Progress",
|
||||
"A call is currently being placed!": "A call is currently being placed!",
|
||||
"Permission Required": "Permission Required",
|
||||
"You do not have permission to start a conference call in this room": "You do not have permission to start a conference call in this room",
|
||||
"End conference": "End conference",
|
||||
|
@ -752,6 +750,13 @@
|
|||
"Notifications": "Notifications",
|
||||
"Enable desktop notifications": "Enable desktop notifications",
|
||||
"Enable": "Enable",
|
||||
"Unknown caller": "Unknown caller",
|
||||
"Voice call": "Voice call",
|
||||
"Video call": "Video call",
|
||||
"Decline": "Decline",
|
||||
"Accept": "Accept",
|
||||
"Sound on": "Sound on",
|
||||
"Silence call": "Silence call",
|
||||
"Use app for a better experience": "Use app for a better experience",
|
||||
"Element Web is experimental on mobile. For a better experience and the latest features, use our free native app.": "Element Web is experimental on mobile. For a better experience and the latest features, use our free native app.",
|
||||
"Use app": "Use app",
|
||||
|
@ -835,6 +840,7 @@
|
|||
"Enable advanced debugging for the room list": "Enable advanced debugging for the room list",
|
||||
"Show info about bridges in room settings": "Show info about bridges in room settings",
|
||||
"New layout switcher (with message bubbles)": "New layout switcher (with message bubbles)",
|
||||
"Don't send read receipts": "Don't send read receipts",
|
||||
"Font size": "Font size",
|
||||
"Use custom size": "Use custom size",
|
||||
"Enable Emoji suggestions while typing": "Enable Emoji suggestions while typing",
|
||||
|
@ -860,6 +866,7 @@
|
|||
"Use Ctrl + F to search timeline": "Use Ctrl + F to search timeline",
|
||||
"Use Command + Enter to send a message": "Use Command + Enter to send a message",
|
||||
"Use Ctrl + Enter to send a message": "Use Ctrl + Enter to send a message",
|
||||
"Surround selected text when typing special characters": "Surround selected text when typing special characters",
|
||||
"Automatically replace plain text Emoji": "Automatically replace plain text Emoji",
|
||||
"Mirror local video feed": "Mirror local video feed",
|
||||
"Enable Community Filter Panel": "Enable Community Filter Panel",
|
||||
|
@ -916,6 +923,17 @@
|
|||
"sends snowfall": "sends snowfall",
|
||||
"Sends the given message with a space themed effect": "Sends the given message with a space themed effect",
|
||||
"sends space invaders": "sends space invaders",
|
||||
"Start the camera": "Start the camera",
|
||||
"Stop the camera": "Stop the camera",
|
||||
"Stop sharing your screen": "Stop sharing your screen",
|
||||
"Start sharing your screen": "Start sharing your screen",
|
||||
"Hide sidebar": "Hide sidebar",
|
||||
"Show sidebar": "Show sidebar",
|
||||
"More": "More",
|
||||
"Dialpad": "Dialpad",
|
||||
"Unmute the microphone": "Unmute the microphone",
|
||||
"Mute the microphone": "Mute the microphone",
|
||||
"Hangup": "Hangup",
|
||||
"unknown person": "unknown person",
|
||||
"Consulting with %(transferTarget)s. <a>Transfer to %(transferee)s</a>": "Consulting with %(transferTarget)s. <a>Transfer to %(transferee)s</a>",
|
||||
"You held the call <a>Switch</a>": "You held the call <a>Switch</a>",
|
||||
|
@ -931,14 +949,6 @@
|
|||
"Fill Screen": "Fill Screen",
|
||||
"Return to call": "Return to call",
|
||||
"%(name)s on hold": "%(name)s on hold",
|
||||
"Unknown caller": "Unknown caller",
|
||||
"Incoming voice call": "Incoming voice call",
|
||||
"Incoming video call": "Incoming video call",
|
||||
"Incoming call": "Incoming call",
|
||||
"Sound on": "Sound on",
|
||||
"Silence call": "Silence call",
|
||||
"Decline": "Decline",
|
||||
"Accept": "Accept",
|
||||
"The other party cancelled the verification.": "The other party cancelled the verification.",
|
||||
"Verified!": "Verified!",
|
||||
"You've successfully verified this user.": "You've successfully verified this user.",
|
||||
|
@ -1149,6 +1159,10 @@
|
|||
"Connecting to integration manager...": "Connecting to integration manager...",
|
||||
"Cannot connect to integration manager": "Cannot connect to integration manager",
|
||||
"The integration manager is offline or it cannot reach your homeserver.": "The integration manager is offline or it cannot reach your homeserver.",
|
||||
"Message layout": "Message layout",
|
||||
"IRC": "IRC",
|
||||
"Modern": "Modern",
|
||||
"Message bubbles": "Message bubbles",
|
||||
"Messages containing keywords": "Messages containing keywords",
|
||||
"Error saving notification preferences": "Error saving notification preferences",
|
||||
"An error occurred whilst saving your notification preferences.": "An error occurred whilst saving your notification preferences.",
|
||||
|
@ -1261,10 +1275,6 @@
|
|||
"Custom theme URL": "Custom theme URL",
|
||||
"Add theme": "Add theme",
|
||||
"Theme": "Theme",
|
||||
"Message layout": "Message layout",
|
||||
"IRC": "IRC",
|
||||
"Modern": "Modern",
|
||||
"Message bubbles": "Message bubbles",
|
||||
"Set the name of a font installed on your system & %(brand)s will attempt to use it.": "Set the name of a font installed on your system & %(brand)s will attempt to use it.",
|
||||
"Enable experimental, compact IRC style layout": "Enable experimental, compact IRC style layout",
|
||||
"Customise your appearance": "Customise your appearance",
|
||||
|
@ -1285,6 +1295,8 @@
|
|||
"Deactivate Account": "Deactivate Account",
|
||||
"Deactivate account": "Deactivate account",
|
||||
"Discovery": "Discovery",
|
||||
"%(brand)s version:": "%(brand)s version:",
|
||||
"Olm version:": "Olm version:",
|
||||
"Legal": "Legal",
|
||||
"Credits": "Credits",
|
||||
"For help with using %(brand)s, click <a>here</a>.": "For help with using %(brand)s, click <a>here</a>.",
|
||||
|
@ -1298,13 +1310,11 @@
|
|||
"FAQ": "FAQ",
|
||||
"Keyboard Shortcuts": "Keyboard Shortcuts",
|
||||
"Versions": "Versions",
|
||||
"%(brand)s version:": "%(brand)s version:",
|
||||
"olm version:": "olm version:",
|
||||
"Copy": "Copy",
|
||||
"Homeserver is": "Homeserver is",
|
||||
"Identity server is": "Identity server is",
|
||||
"Access Token": "Access Token",
|
||||
"Your access token gives full access to your account. Do not share it with anyone.": "Your access token gives full access to your account. Do not share it with anyone.",
|
||||
"Copy": "Copy",
|
||||
"Clear cache and reload": "Clear cache and reload",
|
||||
"Labs": "Labs",
|
||||
"Feeling experimental? Labs are the best way to get things early, test out new features and help shape them before they actually launch. <a>Learn more</a>.": "Feeling experimental? Labs are the best way to get things early, test out new features and help shape them before they actually launch. <a>Learn more</a>.",
|
||||
|
@ -1547,6 +1557,7 @@
|
|||
"Send a reply…": "Send a reply…",
|
||||
"Send an encrypted message…": "Send an encrypted message…",
|
||||
"Send a message…": "Send a message…",
|
||||
"Send voice message": "Send voice message",
|
||||
"The conversation continues here.": "The conversation continues here.",
|
||||
"This room has been replaced and is no longer active.": "This room has been replaced and is no longer active.",
|
||||
"You do not have permission to post to this room": "You do not have permission to post to this room",
|
||||
|
@ -1601,8 +1612,6 @@
|
|||
"Hide Widgets": "Hide Widgets",
|
||||
"Show Widgets": "Show Widgets",
|
||||
"Search": "Search",
|
||||
"Voice call": "Voice call",
|
||||
"Video call": "Video call",
|
||||
"Invites": "Invites",
|
||||
"Favourites": "Favourites",
|
||||
"People": "People",
|
||||
|
@ -1719,14 +1728,11 @@
|
|||
"Invited by %(sender)s": "Invited by %(sender)s",
|
||||
"Jump to first unread message.": "Jump to first unread message.",
|
||||
"Mark all as read": "Mark all as read",
|
||||
"The voice message failed to upload.": "The voice message failed to upload.",
|
||||
"Unable to access your microphone": "Unable to access your microphone",
|
||||
"We were unable to access your microphone. Please check your browser settings and try again.": "We were unable to access your microphone. Please check your browser settings and try again.",
|
||||
"No microphone found": "No microphone found",
|
||||
"We didn't find a microphone on your device. Please check your settings and try again.": "We didn't find a microphone on your device. Please check your settings and try again.",
|
||||
"Record a voice message": "Record a voice message",
|
||||
"Stop the recording": "Stop the recording",
|
||||
"Delete recording": "Delete recording",
|
||||
"Stop recording": "Stop recording",
|
||||
"Error updating main address": "Error updating main address",
|
||||
"There was an error updating the room's main address. It may not be allowed by the server or a temporary failure occurred.": "There was an error updating the room's main address. It may not be allowed by the server or a temporary failure occurred.",
|
||||
"There was an error updating the room's alternative addresses. It may not be allowed by the server or a temporary failure occurred.": "There was an error updating the room's alternative addresses. It may not be allowed by the server or a temporary failure occurred.",
|
||||
|
@ -1888,19 +1894,15 @@
|
|||
"Verification cancelled": "Verification cancelled",
|
||||
"Compare emoji": "Compare emoji",
|
||||
"Connected": "Connected",
|
||||
"You declined this call": "You declined this call",
|
||||
"They declined this call": "They declined this call",
|
||||
"Call declined": "Call declined",
|
||||
"Call back": "Call back",
|
||||
"Call again": "Call again",
|
||||
"This call has ended": "This call has ended",
|
||||
"They didn't pick up": "They didn't pick up",
|
||||
"Missed call": "Missed call",
|
||||
"Could not connect media": "Could not connect media",
|
||||
"Connection failed": "Connection failed",
|
||||
"Their device couldn't start the camera or microphone": "Their device couldn't start the camera or microphone",
|
||||
"An unknown error occurred": "An unknown error occurred",
|
||||
"Unknown failure: %(reason)s)": "Unknown failure: %(reason)s)",
|
||||
"This call has failed": "This call has failed",
|
||||
"You missed this call": "You missed this call",
|
||||
"Retry": "Retry",
|
||||
"The call is in an unknown state!": "The call is in an unknown state!",
|
||||
"Sunday": "Sunday",
|
||||
"Monday": "Monday",
|
||||
|
@ -1923,7 +1925,6 @@
|
|||
"Error processing audio message": "Error processing audio message",
|
||||
"React": "React",
|
||||
"Edit": "Edit",
|
||||
"Retry": "Retry",
|
||||
"Reply": "Reply",
|
||||
"Message Actions": "Message Actions",
|
||||
"Download %(text)s": "Download %(text)s",
|
||||
|
@ -2104,6 +2105,8 @@
|
|||
"%(severalUsers)schanged the server ACLs %(count)s times|one": "%(severalUsers)schanged the server ACLs",
|
||||
"%(oneUser)schanged the server ACLs %(count)s times|other": "%(oneUser)schanged the server ACLs %(count)s times",
|
||||
"%(oneUser)schanged the server ACLs %(count)s times|one": "%(oneUser)schanged the server ACLs",
|
||||
"%(severalUsers)schanged the <a>pinned messages</a> for the room %(count)s times.|other": "%(severalUsers)schanged the <a>pinned messages</a> for the room %(count)s times.",
|
||||
"%(oneUser)schanged the <a>pinned messages</a> for the room %(count)s times.|other": "%(oneUser)schanged the <a>pinned messages</a> for the room %(count)s times.",
|
||||
"Power level": "Power level",
|
||||
"Custom level": "Custom level",
|
||||
"QR Code": "QR Code",
|
||||
|
|
|
@ -311,6 +311,13 @@ export const SETTINGS: {[setting: string]: ISetting} = {
|
|||
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
|
||||
default: null,
|
||||
},
|
||||
"feature_hidden_read_receipts": {
|
||||
supportedLevels: LEVELS_FEATURE,
|
||||
displayName: _td(
|
||||
"Don't send read receipts",
|
||||
),
|
||||
default: false,
|
||||
},
|
||||
"baseFontSize": {
|
||||
displayName: _td("Font size"),
|
||||
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
|
||||
|
@ -449,6 +456,11 @@ export const SETTINGS: {[setting: string]: ISetting} = {
|
|||
displayName: isMac ? _td("Use Command + Enter to send a message") : _td("Use Ctrl + Enter to send a message"),
|
||||
default: false,
|
||||
},
|
||||
"MessageComposerInput.surroundWith": {
|
||||
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
|
||||
displayName: _td("Surround selected text when typing special characters"),
|
||||
default: false,
|
||||
},
|
||||
"MessageComposerInput.autoReplaceEmoji": {
|
||||
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
|
||||
displayName: _td('Automatically replace plain text Emoji'),
|
||||
|
|
|
@ -22,10 +22,11 @@ export interface IToast<C extends ComponentClass> {
|
|||
key: string;
|
||||
// higher priority number will be shown on top of lower priority
|
||||
priority: number;
|
||||
title: string;
|
||||
title?: string;
|
||||
icon?: string;
|
||||
component: C;
|
||||
className?: string;
|
||||
bodyClassName?: string;
|
||||
props?: Omit<React.ComponentProps<C>, "toastKey">; // toastKey is injected by ToastContainer
|
||||
}
|
||||
|
||||
|
|
13
src/theme.js
13
src/theme.js
|
@ -171,15 +171,10 @@ export async function setTheme(theme) {
|
|||
// look for the stylesheet elements.
|
||||
// styleElements is a map from style name to HTMLLinkElement.
|
||||
const styleElements = Object.create(null);
|
||||
let a;
|
||||
for (let i = 0; (a = document.getElementsByTagName("link")[i]); i++) {
|
||||
const href = a.getAttribute("href");
|
||||
// shouldn't we be using the 'title' tag rather than the href?
|
||||
const match = href && href.match(/^bundles\/.*\/theme-(.*)\.css$/);
|
||||
if (match) {
|
||||
styleElements[match[1]] = a;
|
||||
}
|
||||
}
|
||||
const themes = Array.from(document.querySelectorAll('[data-mx-theme]'));
|
||||
themes.forEach(theme => {
|
||||
styleElements[theme.attributes['data-mx-theme'].value.toLowerCase()] = theme;
|
||||
});
|
||||
|
||||
if (!(stylesheetName in styleElements)) {
|
||||
throw new Error("Unknown theme " + stylesheetName);
|
||||
|
|
140
src/toasts/IncomingCallToast.tsx
Normal file
140
src/toasts/IncomingCallToast.tsx
Normal file
|
@ -0,0 +1,140 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { CallType, MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
|
||||
import classNames from 'classnames';
|
||||
import { replaceableComponent } from '../utils/replaceableComponent';
|
||||
import CallHandler, { CallHandlerEvent } from '../CallHandler';
|
||||
import dis from '../dispatcher/dispatcher';
|
||||
import { MatrixClientPeg } from '../MatrixClientPeg';
|
||||
import { _t } from '../languageHandler';
|
||||
import RoomAvatar from '../components/views/avatars/RoomAvatar';
|
||||
import AccessibleTooltipButton from '../components/views/elements/AccessibleTooltipButton';
|
||||
import AccessibleButton from '../components/views/elements/AccessibleButton';
|
||||
|
||||
export const getIncomingCallToastKey = (callId: string) => `call_${callId}`;
|
||||
|
||||
interface IProps {
|
||||
call: MatrixCall;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
silenced: boolean;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.voip.IncomingCallToast")
|
||||
export default class IncomingCallToast extends React.Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
silenced: CallHandler.sharedInstance().isCallSilenced(this.props.call.callId),
|
||||
};
|
||||
}
|
||||
|
||||
public componentDidMount = (): void => {
|
||||
CallHandler.sharedInstance().addListener(CallHandlerEvent.SilencedCallsChanged, this.onSilencedCallsChanged);
|
||||
};
|
||||
|
||||
public componentWillUnmount(): void {
|
||||
CallHandler.sharedInstance().removeListener(CallHandlerEvent.SilencedCallsChanged, this.onSilencedCallsChanged);
|
||||
}
|
||||
|
||||
private onSilencedCallsChanged = (): void => {
|
||||
this.setState({ silenced: CallHandler.sharedInstance().isCallSilenced(this.props.call.callId) });
|
||||
};
|
||||
|
||||
private onAnswerClick = (e: React.MouseEvent): void => {
|
||||
e.stopPropagation();
|
||||
dis.dispatch({
|
||||
action: 'answer',
|
||||
room_id: CallHandler.sharedInstance().roomIdForCall(this.props.call),
|
||||
});
|
||||
};
|
||||
|
||||
private onRejectClick= (e: React.MouseEvent): void => {
|
||||
e.stopPropagation();
|
||||
dis.dispatch({
|
||||
action: 'reject',
|
||||
room_id: CallHandler.sharedInstance().roomIdForCall(this.props.call),
|
||||
});
|
||||
};
|
||||
|
||||
private onSilenceClick = (e: React.MouseEvent): void => {
|
||||
e.stopPropagation();
|
||||
const callId = this.props.call.callId;
|
||||
this.state.silenced ?
|
||||
CallHandler.sharedInstance().unSilenceCall(callId) :
|
||||
CallHandler.sharedInstance().silenceCall(callId);
|
||||
};
|
||||
|
||||
public render() {
|
||||
const call = this.props.call;
|
||||
const room = MatrixClientPeg.get().getRoom(CallHandler.sharedInstance().roomIdForCall(call));
|
||||
const isVoice = call.type === CallType.Voice;
|
||||
|
||||
const contentClass = classNames("mx_IncomingCallToast_content", {
|
||||
"mx_IncomingCallToast_content_voice": isVoice,
|
||||
"mx_IncomingCallToast_content_video": !isVoice,
|
||||
});
|
||||
const silenceClass = classNames("mx_IncomingCallToast_iconButton", {
|
||||
"mx_IncomingCallToast_unSilence": this.state.silenced,
|
||||
"mx_IncomingCallToast_silence": !this.state.silenced,
|
||||
});
|
||||
|
||||
return <React.Fragment>
|
||||
<RoomAvatar
|
||||
room={room}
|
||||
height={32}
|
||||
width={32}
|
||||
/>
|
||||
<div className={contentClass}>
|
||||
<span className="mx_CallEvent_caller">
|
||||
{ room ? room.name : _t("Unknown caller") }
|
||||
</span>
|
||||
<div className="mx_CallEvent_type">
|
||||
<div className="mx_CallEvent_type_icon" />
|
||||
{ isVoice ? _t("Voice call") : _t("Video call") }
|
||||
</div>
|
||||
<div className="mx_IncomingCallToast_buttons">
|
||||
<AccessibleButton
|
||||
className="mx_IncomingCallToast_button mx_IncomingCallToast_button_decline"
|
||||
onClick={this.onRejectClick}
|
||||
kind="danger"
|
||||
>
|
||||
<span> { _t("Decline") } </span>
|
||||
</AccessibleButton>
|
||||
<AccessibleButton
|
||||
className="mx_IncomingCallToast_button mx_IncomingCallToast_button_accept"
|
||||
onClick={this.onAnswerClick}
|
||||
kind="primary"
|
||||
>
|
||||
<span> { _t("Accept") } </span>
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</div>
|
||||
<AccessibleTooltipButton
|
||||
className={silenceClass}
|
||||
onClick={this.onSilenceClick}
|
||||
title={this.state.silenced ? _t("Sound on") : _t("Silence call")}
|
||||
/>
|
||||
</React.Fragment>;
|
||||
}
|
||||
}
|
|
@ -43,9 +43,8 @@ function getManagedIframe(): { iframe: HTMLIFrameElement, onLoadPromise: Promise
|
|||
|
||||
// Dev note: the reassignment warnings are entirely incorrect here.
|
||||
|
||||
// @ts-ignore
|
||||
// noinspection JSConstantReassignment
|
||||
managedIframe.style = { display: "none" };
|
||||
managedIframe.style.display = "none";
|
||||
|
||||
// @ts-ignore
|
||||
// noinspection JSConstantReassignment
|
||||
managedIframe.sandbox = "allow-scripts allow-downloads allow-downloads-without-user-activation";
|
||||
|
|
|
@ -16,6 +16,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import { _t } from '../languageHandler';
|
||||
import { jsxJoin } from './ReactUtils';
|
||||
|
||||
/**
|
||||
* formats numbers to fit into ~3 characters, suitable for badge counts
|
||||
|
@ -103,7 +104,7 @@ export function getUserNameColorClass(userId: string): string {
|
|||
* @returns {string} a string constructed by joining `items` with a comma
|
||||
* between each item, but with the last item appended as " and [lastItem]".
|
||||
*/
|
||||
export function formatCommaSeparatedList(items: string[], itemLimit?: number): string {
|
||||
export function formatCommaSeparatedList(items: Array<string | JSX.Element>, itemLimit?: number): string | JSX.Element {
|
||||
const remaining = itemLimit === undefined ? 0 : Math.max(
|
||||
items.length - itemLimit, 0,
|
||||
);
|
||||
|
@ -113,9 +114,9 @@ export function formatCommaSeparatedList(items: string[], itemLimit?: number): s
|
|||
return items[0];
|
||||
} else if (remaining > 0) {
|
||||
items = items.slice(0, itemLimit);
|
||||
return _t("%(items)s and %(count)s others", { items: items.join(', '), count: remaining } );
|
||||
return _t("%(items)s and %(count)s others", { items: jsxJoin(items, ', '), count: remaining } );
|
||||
} else {
|
||||
const lastItem = items.pop();
|
||||
return _t("%(items)s and %(lastItem)s", { items: items.join(', '), lastItem: lastItem });
|
||||
return _t("%(items)s and %(lastItem)s", { items: jsxJoin(items, ', '), lastItem: lastItem });
|
||||
}
|
||||
}
|
||||
|
|
33
src/utils/ReactUtils.tsx
Normal file
33
src/utils/ReactUtils.tsx
Normal file
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
|
||||
/**
|
||||
* Joins an array into one value with a joiner. E.g. join(["hello", "world"], " ") -> <span>hello world</span>
|
||||
* @param array the array of element to join
|
||||
* @param joiner the string/JSX.Element to join with
|
||||
* @returns the joined array
|
||||
*/
|
||||
export function jsxJoin(array: Array<string | JSX.Element>, joiner?: string | JSX.Element): JSX.Element {
|
||||
const newArray = [];
|
||||
array.forEach((element, index) => {
|
||||
newArray.push(element, (index === array.length - 1) ? null : joiner);
|
||||
});
|
||||
return (
|
||||
<span>{ newArray }</span>
|
||||
);
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue