Compare commits

...

181 Commits

Author SHA1 Message Date
paweldomas
c2191e3a28 callkit with base/session 2018-08-07 12:20:38 -05:00
paweldomas
72e3e8593d feat(base/session): store 'room' in the session
Stores name of the conference room in the session when it's being
created.
2018-08-07 12:20:38 -05:00
paweldomas
67a8b4915d feat(base/session): add SESSION_CONFIGURED event
The SESSION_CONFIGURED event is fired once the config has been set,
after either being loaded or restored from the storage.
2018-08-07 12:20:38 -05:00
paweldomas
468d4a7150 ref(mobile/external-api): use base/session 2018-08-07 12:20:38 -05:00
paweldomas
2a01e29fec feat: add features/base/session 2018-08-07 12:20:38 -05:00
paweldomas
90a64d30dc ref(base/config): keep 'locationURL' after SET_CONFIG
This is required for the session feature to be able to tell what's
the latest URL the app is working with.
2018-08-07 12:20:38 -05:00
paweldomas
31905d4f63 debug actions 2018-08-07 12:20:38 -05:00
Nik
7e1d97665a fix: only access nested json values when corrent payload type (#3352) 2018-08-07 09:03:31 -07:00
Zoltan Bettenbuk
b978851a0f [RN] Fix streaming on mobile (#3351) 2018-08-06 17:30:32 -07:00
Nik
ef49817eaf fix: add a timer which automatically clears subtitles (#3349) 2018-08-06 14:30:50 -07:00
virtuacoplenny
cac8888b37 feat(welcome-page): be able to open settings dialog (#3327)
* feat(welcome-page): be able to open settings dialog

- Create a getter for getting a settings tab's props so the device
  selection tab can get updated available devices.
- Be able to call a function from a tab after it has mounted. This is
  used for device selection to essentially call enumerateDevices on
  the welcome page so the device selectors are populated.
- Remove event UIEvents.AUDIO_OUTPUT_DEVICE_CHANGED. Instead directly call
  setAudioOutputDeviceId where possible.
- Fix initialization of the audioOutputDeviceId in settings by defaulting
  the audio output device to the one set in settings.

* squash: updateAvailableDevices -> getAvailableDevices, add comment for propsUpdateFunction
2018-08-06 10:24:59 -05:00
Praveen Gupta
81853d971a [WEB] Show final translated speech to text results as subtitles (#3276)
* Shows final translated speech to text results as subtitles

* Use conference from redux state and removes addTranscriptMessage
2018-08-06 11:24:37 +02:00
Lyubo Marinov
b9c5ed3b03 Fixes typo, comment 2018-08-05 17:18:14 -05:00
Lyubo Marinov
0892e0b644 Remove duplication 2018-08-05 17:04:19 -05:00
Bettenbuk Zoltan
b41bf22be7 Replace console with logger 2018-08-05 17:04:19 -05:00
Saúl Ibarra Corretgé
a1cc9bce91 [RN] Drop no longer needed polyfills
They were required only on Android because of its old JSC version. With the JSC
version bump they are no longer required.
2018-08-05 17:04:19 -05:00
Saúl Ibarra Corretgé
8d3cecad86 [Android] Update JSC version
The JSC version used by React Native is about 3 years old, and doesn't implement
things like Symbol or Typed Arrays, which require polyfills. These polyfills are
sometimes a los less performant, as is the case for Typed Arrays.

Bumping an updated JSC version makes both platforms consistent when it comes to
the JavaScript platform.
2018-08-05 17:04:16 -05:00
hristoterezov
bd8559fad6 fix(invite): IFrame api when invalid invitees are passed. 2018-08-03 12:42:38 -05:00
hristoterezov
fb75180632 ref(RecentList): Improvements after review. 2018-08-03 11:25:03 -05:00
Ritwik Heda
046b06e436 added recent list 2018-08-03 11:25:03 -05:00
Дамян Минков
af7c69a1aa Moves google-api in its own feature. (#3339)
* Moves google-api in its own feature.

* Stores the profile email in redux.
2018-08-02 14:56:36 -07:00
Saúl Ibarra Corretgé
7ad0639f7a [RN] Fix setting audio mode for audio-only calls
When a call is tarted in audio only mode due to the switch on the welcome page,
the wrong audio mode was chosen.
2018-08-01 22:12:08 +02:00
paweldomas
54a1853e60 fix(ios/Podfile.lock): bump SDWebImage/Core version 2018-07-31 14:07:17 -05:00
Saúl Ibarra Corretgé
27021ea271 [RN] Replace cached image implementation
Use react-native-fastimage, which uses 2 full-native image impleentations using
well known and mature (native) libraries.

This gets us rid of 2 libraries which were observerd as a source of bugs and
created trouble with dependencies: react-native-fetch-blob and
react-native-img-cache. They are also no longer well maintained.
2018-07-31 14:07:17 -05:00
Saúl Ibarra Corretgé
f5a667ad9e feat(Avatar): simplified code 2018-07-31 14:07:17 -05:00
paweldomas
2b9ce40533 feat(travis): bump image version 2018-07-31 12:54:01 -05:00
paweldomas
d3dd833f21 fix(ios/travis-ci) try pod update
With the fastimage lib Travis complains about:

CocoaPods could not find compatible versions for pod "SDWebImage/Core"
2018-07-31 12:54:01 -05:00
Neil Brown
1cc372868b Update quick-install.md
Tweaks for clarity.
2018-07-31 11:35:18 +02:00
bgrozev
a6956c7c34 Commit from translate.jitsi.org by user bgrozev.: 447 of 447 strings translated (0 fuzzy). 2018-07-30 14:27:03 -05:00
Leonard Kim
aaaa3e05d1 ref(thumbnail): pass in position of remote menu popover 2018-07-30 11:48:52 -05:00
Saúl Ibarra Corretgé
467a5aaae3 ios: run audio mode operations on a dedicated thread
There is no reason for them to run on the main thread, it's safe to call
AVFoundation functions on threads other than the main thread.

The previous code made an incorrect claim about the thread in which the audio
route change notification selector is called: it's called on a secondary thread:
https://developer.apple.com/documentation/avfoundation/avaudiosessionroutechangenotification
2018-07-27 15:39:39 -05:00
Saúl Ibarra Corretgé
243dd16285 android: run all audio and bluetooth operation on a dedicated thread 2018-07-27 15:39:39 -05:00
Saúl Ibarra Corretgé
92001f4d37 android: run WiFi stats operations on a dedicated thread 2018-07-27 15:39:39 -05:00
paweldomas
6a31c59081 ref(media/VideoTrack.native): remove fade animation 2018-07-27 12:08:54 +02:00
paweldomas
11c5b220a1 fix(participants/Avatar.native): disable fade animation
The Image adds a fade effect without asking, so lets explicitly disable
it. More info here:
https://github.com/facebook/react-native/issues/10194
2018-07-27 12:08:54 +02:00
virtuacoplenny
590ad90cd1 ref(video-layout): resize thumbnails first when resizing video area (#3308) 2018-07-26 11:45:04 -07:00
Nik
ca62e902bc Merge pull request #3312 from nikvaessen/master
comment out transcribingEnabled property; in code defaults to false
2018-07-26 19:51:38 +02:00
virtuacoplenny
34d1eb6768 ref(filmstrip): create an empty container for local filmstrip move (#3303)
* ref(filmstrip): create an empty container for local filmstrip move

This might be necessary for tile view. To support making the
local video display at the end of remote videos while in tile
view, but separateed from scrollable remote videos, moving
the local video might be necessary. By creating an empty
container, there is a target for local video to move to.

* squash: rename id
2018-07-26 12:51:15 -05:00
Nik Vaessen
b6b21e5410 comment out transcribingEnabled property; in code defaults to false 2018-07-26 19:10:40 +02:00
Nik
b8daf0a9f9 [WEB] add UI for transcription (#3213)
* [WEB] add UI for transcription

* add analytics event for button, do not use global APP object

* use props instead of state, use local conference to kick participant

* put imports in alphabetical order

* add translation for TranscribingLabel

* fix merge conflict

* add closed caption button

* purge OverFlowMenuItem which starts and stops Transcription

* readd closed caption icon and fix small issues due to purge

* delete unused icon in _font.scss
2018-07-26 09:33:40 -07:00
virtuacoplenny
39f1958300 ref(filmstrip): apply filmstrip class to Conference root (#3294)
* ref(filmstrip): apply filmstrip class to Conference root

Instead of apply the layout class to the body, it can be
applied to Conference. This will allow easier switching
between tile filmstrip and horizontal/vertical filmstrip.

* squash: fix typo filstrip
2018-07-25 13:00:00 -07:00
Leonard Kim
0b1224495b ref(video-quality): update video quality post redux update
Move away from middleware and instead update video quality
when the selected video quality updates in redux. This also
lead to removing of automatically exiting audio only because
with the change it's not so readily possible to tell if the
user switched off audio only by re-selecting the already
preferred video quality. Removing this automagic removed
some additional checking done for mobile.
2018-07-25 12:17:13 -07:00
Leonard Kim
ee7d180cbb feat(video-quality): be able to set an internal max
The internal max will be used for tile view. Whatever the
user has set for preferred video quality, the internal
maximum will be respected. This allows for the case where
the user prefers high definition video, but in tile view
it only makes sense to send low definition; ux wise the
user is allowed to continue messing with the video quality
slider.
2018-07-25 12:17:13 -07:00
Leonard Kim
4d3383c620 ref(video-quality): rename receiveVideoQuality to preferredReceiverVideoQuality
- "preferred" is being appended because in tile view there is a
  concept of what the user prefers to be the maximum video quality
  but there is also a maximum respected internall. For example,
  the user may prefer HD, but in tile view the tiles may be small
  so internall the preferred would be set to LD.
- "receive" is being renamed to "receiver" to be consistent with
  the naming in lib-jitsi-meet.
2018-07-25 12:17:13 -07:00
Pablo Saavedra
fd78203ff8 noticeMessage is not shown (refs #3295)
* Get back the Notice class
* Add Notice component in the Conference web view
* Notice is not exported in index.js. Only used internally by
  Conference.
* noticeMessage value obtained from features/base/config
  * using mapStateToProps
  * value is stored in the internal _message property
* Notice component, orignal in `toolbox` is moved from
  `toolbox/components` to `conference/components`
* Notice component only implemented and renderable in web views
* Dummy `conference/components/Notice.naive.js`

This patch is partially based in the removed logic included
originally in:

    commit 59a74153dc
    (tag: jitsi-meet_1886, tag: jitsi-meet_1885, tag: 1797, tag: 1796)
    Author: Ilya Daynatovich <shupuercha@gmail.com>
    Date:   Mon Mar 20 11:04:54 2017 -0500

      Toolbar notice as React Component

In reply to: Saúl Ibarra Corretgé @saghul> comments

Signed-off-by: Pablo Saavedra <psaavedra@igalia.com>
2018-07-25 14:16:47 -05:00
virtuacoplenny
a36b341865 ref(popover): allow for popover content from the right (#3302)
* ref(popover): allow for popover content from the right

Popovers contents can display to the left of the trigger
and above the trigger. Add the ability to display to the
right of the trigger my adding mouseover padding. This
may be needed for tile view, depending on where the triggers
are located.

* squash: abstract common css proprties into placeholder class
2018-07-25 13:28:36 -05:00
Saúl Ibarra Corretgé
3d6e18394e deps: update url-polyfill dependency
The previous location no longer exists. This is a fork of the original package,
which is actively maintained.

Fixes: #3304
2018-07-25 11:27:54 -05:00
virtuacoplenny
9a6e5c67f5 feat(tile-view): add new toolbar icon (#3292) 2018-07-25 08:22:18 -07:00
virtuacoplenny
50ea847905 Refactor welcome page in prep for branding (#3230)
* fix(welcome-page): css tweaks in prep for branded welcome page

- Watermarks should no longer depend on toolbar size. The left watermark made
  room for the toolbar when the toolbar was on the left side of the screen, but
  the toolbar has been moved to the bottom. The right watermark...well it'll
  clash with the vertical filmstrip but at least the margins will be consistent
  with the left watermark.
- Apply new font-family so fonts are more likely to be consistent across the
  app. Design likes SF UI and keeps requesting it so use it by default.
- Change sizings of welcome page header to be more responsive. This will help
  the header be scrollable when there is no additional content and the header
  overflows.
- Change colors of the welcome page header and remove background image that
  was in the header. Leave in the dom for the background image in case other
  deployments need to continue showing an image.
- Add a period to the title of the welcome page.
- Move watermarks dom location as it is not part of the header; it's part of the
  whole page.

* [squash] Size and font adjustments. Renaming.
2018-07-24 14:26:17 -05:00
virtuacoplenny
b54a9e2bf7 chore(deps): update lib for selecting participants and maxFrameHeight caching (#3291) 2018-07-23 14:20:30 -07:00
virtuacoplenny
918fb1dfc6 ref(utils): use web reportError helper (#3283) 2018-07-21 08:16:32 -07:00
Дамян Минков
9f015df61d Commit from translate.jitsi.org by user damencho.: 446 of 447 strings translated (0 fuzzy). (#3257) 2018-07-20 15:07:29 -05:00
Leonard Kim
2cd1b7f80b fix(presence-label): set position for small video presence label only 2018-07-20 13:27:28 -05:00
virtuacoplenny
afd2aea79c ref(large-video): combine selectParticipant logic from web (#3266)
* ref(large-video): combine selectParticipant logic from web

Currently native/middleware/redux has its own logic for selecting a participant
on the bridge. To have the logic web respect that logic, a few changes are
needed.
- Web no longer has its own call to selectParticipant.
- To keep in line with web logic selectParticipant action should act even when
  there is no track. This makes it so that when a participant does get a track
  that the bridge will send high quality. The bridge can already handle when the
  selected participant does not have a video track.
- The timing of web is such that on joining an existing conference, a
  participant joins and the participant's tracks get updated and then the
  conference is joined. The result is selectParticipant does not get fired
  because it no-ops when there is no conference. To avoid having to make
  uncertain changes (to be lazy), update the selected participant on conference
  join as well.

* squash: update comment, pass message to error handler
2018-07-20 13:19:26 -05:00
virtuacoplenny
c62f761d67 fix(dial-in): allow scroll on dial in info page (#3271)
* fix(dial-in): allow scroll on dial in info page

* squash: some more tweaks for flexible sizing
2018-07-20 10:32:28 -05:00
Boris Grozev
acda279111 npm: Updates lib-jitsi-meet. 2018-07-19 17:10:28 -05:00
Daniel Ornelas
ccf9e2a362 deps: update react-native-webrtc
This version uses a worker queue for all WebRTC operations in iOS.
2018-07-19 18:35:56 +02:00
Saúl Ibarra Corretgé
3154c6f936 [RN] Don't request camera permission on first launch
It will only be requested if a user joins a meeting or flips the switch from
video to audio and back, but never as the first thing when the welcome page is
mounted.
2018-07-19 09:03:22 -05:00
Lyubo Marinov
8ff3ae0ab2 [Android] Introduce IncomingCallView (continued) 2018-07-18 22:47:18 -05:00
Saúl Ibarra Corretgé
ea22d12581 [Android] Introduce IncomingCallView
It's a separate view (on the native side) and app (on the JavaScript side) so
applications can use it independently.

Co-authored-by: Shuai Li <sli@atlassian.com>
Co-authored-by: Pawel Domas <pawel.domas@jitsi.org>
2018-07-18 22:47:18 -05:00
Saúl Ibarra Corretgé
39e236a42c feat(external_api): export sendEvent function
Small reorganization so other features can send events to the native side.
2018-07-18 22:47:18 -05:00
paweldomas
01c2786c95 ref(base/util): move getSymbolDescription to util 2018-07-18 22:47:18 -05:00
Saúl Ibarra Corretgé
9972e88b67 [Android] Split base functionality out of JitsiMeetView
As the need for adding more views connected with our React code arises, having
everything in JitsiMeetView is not going to scale.

In order to pave the way for multiple apps / views feeding off the React side,
the following changes have been made:

- All base functionality related to creating a ReactRootView and layout are now
  in BaseReactView
- All Activity lifecycle methods that need to be called by any activity holding
  a BaseReactView are now conveniently placed in ReactActivityLifecycleAdapter
- ExternalAPIModule has been refactored to cater for multiple views: events are
  delivered to views, and its their resposibility to deal with them
- Following on the previous point, ListenerUtils is a utility class for helping
  with the translation from events into listener methods
2018-07-18 22:47:18 -05:00
Дамян Минков
cd1c384cc8 Enables live-streaming for guests. (#3274) 2018-07-18 18:11:54 -07:00
Leonard Kim
f97f294d1a feat(live-streaming): add beta tag to mobile 2018-07-18 10:42:14 +02:00
Nik
d3dd54ac3b Show subtitles when Jigasi sends transcription results in JSON (#1914)
* Show subtitles when Jigasi sends transcription results in JSON

* fix: Import PropTypes from prop-types.

* apply feedback on initial PR

* Changed Object to Map, alphabetic ordering fixes ,css changes in transcription subtitles

* Sends Map of transcriptMessages as prop to Component

* Documentation fixes and uses config in redux state

* Minor doc fix

* rename feature 'transcription' to 'subtitles'

* Moves subtitles config to interfaceConfig and minor fixes

* minor lint fix
2018-07-17 12:31:12 -05:00
Saúl Ibarra Corretgé
13ee67d15c config: default to 720p (#3269) 2018-07-17 08:18:32 -07:00
Saúl Ibarra Corretgé
b25caedce7 feat(eslint): fix 2 eslint warnings (#3268) 2018-07-17 08:08:22 -07:00
Leonard Kim
5d4a2e87f8 fix(device-selection): use persisted settings as default values if available 2018-07-16 20:38:04 -07:00
Leonard Kim
44baca3185 fix(device-selection): pass dispatch so preferred speaker is saved 2018-07-16 20:38:04 -07:00
virtuacoplenny
b9f28a1beb fix(live-streaming): add beta tag to toolbar button (#3263) 2018-07-16 19:15:34 -07:00
Aaron van Meerten
6b7a883331 Merge pull request #3265 from jmacelroy/table-async-wrapper
Creating a new async prosody http wrapper.
2018-07-16 17:01:48 -05:00
jmacelroy
944cf4272d Creating a new async prosody http wrapper. 2018-07-16 21:58:48 +00:00
Aaron van Meerten
cc27e96b22 Merge pull request #3264 from jmacelroy/missed-calls
feat(calls): Adding missed call event triggering.
2018-07-16 12:52:58 -05:00
virtuacoplenny
4e4755f91e Remove state from mediaDeviceHelper (#3226)
* ref(device-selection): do not override var that is not reference again

* ref(device-selection): do not override var that is not reference again

* ref(device-selection): always update known devices on device list update

* ref(device-selection): replace call to get devices from legacy to redux

* ref(device-selection): remove unused device list state from mediaDeviceHelper

* ref(device-selection): update store before updating UI
2018-07-13 10:31:28 -07:00
virtuacoplenny
0dcf8ef2f6 fix(device-selection): add hover color for device output test (#3254) 2018-07-13 10:08:35 -07:00
Saúl Ibarra Corretgé
1ee71be961 [RN] Kill some dead code 2018-07-13 10:01:39 -05:00
Lyubo Marinov
bfdfb5321c feat(App): refactor App and split it into BaseApp and App (continued)
There doesn't seem to be a strong need for the initialized React
Component state in BaseApp so remove/delete it.
2018-07-12 11:28:48 -05:00
Saúl Ibarra Corretgé
dc246960df feat(App): refactor App and split it into BaseApp and App
BaseApp does all the heavy-lifting related to creating the redux store,
navigation, and so on.

App currently handles URL props and actually triggering navigation based on
them.
2018-07-12 11:28:19 -05:00
Saúl Ibarra Corretgé
3bfab7718f [RN] Refactor getting the default URL
Move it away from AbstractApp into an auxiliary function. In addition, introduce
a new `getServerURL` function which gets the configured server URL and defaults
to meet.jit.si as before.
2018-07-12 11:28:18 -05:00
Saúl Ibarra Corretgé
980648df4d feat(App): remove ability to specify an external redux store
It was never used in practice, and it would be very cumbersome to use, since it
would have to bcreated with all the middlewares and reducers we need. After
discussing this with Lyubomir, we are confident this is not going to be needed
so it can go.
2018-07-12 11:28:18 -05:00
Saúl Ibarra Corretgé
f2f991e969 feat(App): move participant leaving logic to base/participants 2018-07-12 11:28:17 -05:00
Bettenbuk Zoltan
96a837801e [RN] Tint active speaker thumbnail 2018-07-12 09:43:29 +02:00
Lyubo Marinov
9c4da125c8 lib-jitsi-meet "core: refactor initialization not to return a Promise (continued)" 2018-07-12 00:05:40 -05:00
Lyubo Marinov
c203215c54 core: refactor routing (continued) 2018-07-11 22:58:41 -05:00
Saúl Ibarra Corretgé
155e02bbfb core: refactor routing
Unfortunately, as the Jitsi Meet development evolved the routing mechanism
became more complex and thre logic ended up spread across multiple parts of the
codebase, which made it hard to follow and extend.

This change aims to fix that by rewriting the routing logic and centralizing it
in (pretty much) a single place, with no implicit inter-dependencies.

In order to arrive there, however, some extra changes were needed, which were
not caught early enough and are thus part of this change:

- JitsiMeetJS initialization is now synchronous: there is nothing async about
  it, and the only async requirement (Temasys support) was lifted. See [0].
- WebRTC support can be detected early: building on top of the above, WebRTC
  support can now be detected immediately, so take advantage of this to simplify
  how we handle unsupported browsers. See [0].

The new router takes decissions based on the Redux state at the time of
invocation. A route can be represented by either a component or a URl reference,
with the latter taking precedence. On mobile, obviously, there is no concept of
URL reference so routing is based solely on components.

[0]: https://github.com/jitsi/lib-jitsi-meet/pull/779
2018-07-11 22:58:41 -05:00
jmacelroy
d189888902 feat(calls): Adding missed call event triggering. 2018-07-11 21:09:53 +00:00
Bettenbuk Zoltan
5aee082bf9 [RN] Implement streaming on mobile 2018-07-11 15:13:16 -05:00
Saúl Ibarra Corretgé
453c4b99dc cleanup: drop polyfills which were required for IE11 2018-07-11 17:53:32 +02:00
Bettenbuk Zoltan
961e1d611f [RN] Only ask for calendar permission on user interaction 2018-07-11 17:17:24 +02:00
hristoterezov
bd449be20d fix(VideoLayout): JS error if updateLargeVideo is called too early. 2018-07-09 20:22:43 -05:00
hristoterezov
9331b0870b fix(presence-label):styles 2018-07-09 20:22:43 -05:00
hristoterezov
00d1edcdef fix(jwt): import for mobile. 2018-07-09 20:22:43 -05:00
hristoterezov
769e782c6f feat(callee-info): Redesign. 2018-07-09 20:22:43 -05:00
virtuacoplenny
485ff81443 fix(hangup): truthy check for deviceChangeListener before removing it (#3235)
It can be that deviceChangeListener is never defined because
the isDeviceList call never completes. On hangup, that would
cause an error to be thrown within lib-jitsi-meet because of
an attempt to remove an undefined event handler. That is
what happens on Safari right now.
2018-07-09 11:46:26 -07:00
damencho
d12afc5c07 Fixes the room size api which returns string result back to client. 2018-07-09 13:44:24 -05:00
akshitkrnagpal
20444adbc9 Added emailChange listener to API 2018-07-09 10:14:27 -05:00
Zoltan Bettenbuk
63c017f8e6 Fix persistency to handle default values too (#3228) 2018-07-06 11:03:16 -07:00
virtuacoplenny
afe7c4470d feat(small-video): add flag to hide connection indicators (#3225) 2018-07-06 08:24:38 -07:00
Дамян Минков
3f3a957f40 Removes unneeded translation. (#3217) 2018-07-03 13:34:43 -07:00
Emil Ivov
26c0164f1e Merge pull request #3211 from saghul/audio-route-copy
[RN] Update audio route selection copy
2018-07-03 19:01:06 +03:00
Bettenbuk Zoltan
b48c897d9b [WEB] Move RecordButton to the new ToolBox abstraction layer 2018-07-03 11:08:37 +01:00
Bettenbuk Zoltan
e59761baa2 Implement ToolboxItem features: disabled, tooltip with label 2018-07-03 11:08:37 +01:00
damencho
baa2c217de Syncs with latest lib-jitsi-meet (98acf13).
Detects msid changes and signals them, part of fixing start A/V muted using Firefox.
2018-07-02 22:41:00 -05:00
Leonard Kim
5dc2aca081 fix(video-layout): handle undefined video type for large video update
When replace track is called in JitsiConference, there is no
guarantee a videoType update will come in presence before
the track added event. This can lead to the situation in
LargeVideoManager where an update is called with a track
with an undefined videoType.
2018-07-02 21:54:16 -05:00
virtuacoplenny
84b589719f fix(connection): reload immediately on possible split-brain (#3162)
* fix(connection): reload immediately on possible split-brain

There isn't an explicit way to know when a split brain
scenario has happened. It is assumed it arises when an
"item-not-found" connection error is encountered early
on in the conference. So, store when a connection has
happened so it be calculated how much time has
elapsed and if the threshold has not been exceeded
then do an immediate reload of the app instead of
showing the overlay with a reload timer.

* squash: rename isItemNotFoundError -> isShardChangedError
2018-07-02 16:22:51 -05:00
jmacelroy
1c6d22b75e Adding state to poltergeist store for correlating external resources with calls. 2018-06-29 14:51:48 -05:00
Leonard Kim
2547ee3a04 ref(filmstrip): use explicit class for horizontal filmstrip
This will make it easier to support horizontal, vertical, and
tile layout filmstrip by reducing the css overriding needed
for tile layout.
2018-06-29 20:11:59 +01:00
Leonard Kim
7328dd9125 ref(filmstrip): add class to body for horizontal filmstrip 2018-06-29 20:11:59 +01:00
Leonard Kim
0aa2d81844 ref(filmstrip): move vertical filmstrip container styles to own file 2018-06-29 20:11:59 +01:00
Leonard Kim
e1f7d4585e ref(filmstrip): move some video container overrides 2018-06-29 20:11:59 +01:00
Leonard Kim
bdae4b9493 ref(filmstrip): remove ie11 css flex hack 2018-06-29 20:11:59 +01:00
Leonard Kim
8f688c3535 ref(filmstrip): move around small video and quality label styles 2018-06-29 20:11:59 +01:00
Leonard Kim
60ae8497c0 ref(filmstrip): move some small video specific styling to own file 2018-06-29 20:11:59 +01:00
Leonard Kim
7c1b7a588e ref(filmstrip): move filmstrip styles to filmstrip folder 2018-06-29 20:11:59 +01:00
Leonard Kim
fd05f120ff ref(filmstrip): move vertical filmstrip overrides to new filmstrip folder 2018-06-29 20:11:59 +01:00
Leonard Kim
d9fa05f42e ref(filmstrip): move toolbar css to own file 2018-06-29 20:11:59 +01:00
Leonard Kim
12901be6be ref(filmstrip): move presence label styles with similar styles 2018-06-29 20:11:59 +01:00
Zoltan Bettenbuk
009eeccf3c Merge pull request #3142 from virtuacoplenny/lenny/new-audio-output-icon
feat(device-selection): new icon for audio output
2018-06-29 14:50:46 +02:00
Zoltan Bettenbuk
e01acd9cf0 Merge pull request #3187 from virtuacoplenny/lenny/cleanup-new-toolbox-css
ref(toolbar): remove use-new-toolbox class
2018-06-29 14:48:15 +02:00
Bettenbuk Zoltan
ac63a0fa73 Calendar feature disabled state getter
This commit adds a state getter that considers checking the enabled/disabled state of the calendar feature, so then other features don’t have to do it manually.
2018-06-29 09:36:34 +01:00
Saúl Ibarra Corretgé
1d296f9704 [RN] Update audio route selection copy 2018-06-29 10:08:04 +02:00
Leonard Kim
98e3bcb691 feat(device-selection): new icon for audio output 2018-06-28 14:59:07 -07:00
Leonard Kim
880d3525db squash: pass class name into filmstrip 2018-06-28 11:06:10 -07:00
Leonard Kim
c958c64ba8 ref(toolbar): remove use-new-toolbox class
Very likely I broke something subtle and I'm prepared to fix it.
2018-06-28 11:06:10 -07:00
bgrozev
cc319ad5e9 chore: Updates the callstats lib. (#3207) 2018-06-28 17:40:33 +01:00
Saúl Ibarra Corretgé
75b2eb0f99 deps: update lib-jitsi-meet dependency 2018-06-28 13:55:12 +02:00
Saúl Ibarra Corretgé
acb3bd7ad7 feat(BrowserSupport): remove PluginRequiredBrowser
WebRTC plugin support has been axed, this is now dead code.
2018-06-28 13:55:12 +02:00
Saúl Ibarra Corretgé
7fcc95c9da feat(UnsupportedDesktopBrowser): recommend Edge, not IE 2018-06-28 13:55:12 +02:00
Saúl Ibarra Corretgé
87fa8de815 feat(sanity): axe IE and Temasys plugin support 🔥🔥🔥 2018-06-28 13:55:12 +02:00
Bettenbuk Zoltan
7164cd49e4 [RN] Implement Recording on mobile 2018-06-28 12:47:50 +02:00
Bettenbuk Zoltan
4ac367d403 [RN] Implement Labels on mobile 2018-06-28 12:47:50 +02:00
Bettenbuk Zoltan
ffd0827354 [RN] Implement Notifications on mobile 2018-06-28 12:47:50 +02:00
Sam Joseph
00f18e9369 doc: minor grammatical fix (#3202) 2018-06-28 11:07:47 +02:00
jmacelroy
401c43ee02 fix: Properly setting poltergeist ignore status. 2018-06-27 17:28:20 -05:00
Jacob MacElroy
6ae5adcb3d Creating a poltergiest library and using in for mod_muc_poltergeist. 2018-06-27 11:59:38 -05:00
Saúl Ibarra Corretgé
535e5b4f64 [iOS] Update Podfile.lock
Syntax changed slightly with an update to CocoaPods.
2018-06-26 17:59:02 +02:00
Saúl Ibarra Corretgé
a5d0bfe1d4 Merge pull request #3179 from jitsi/reload_screen_a_catch_all
[RN] Reload screen a catch all
2018-06-26 17:07:06 +02:00
paweldomas
67d7d4fc14 feat(RN): add a fatal error state which is a catch all
Adds a fatal error state on which will depend whether or not the reload
screen is to be displayed. It is to happen when a relevant fatal error
action is not claimed by any feature for error recovery (the recoverable
flag is not set).
2018-06-26 15:39:56 +02:00
Lyubo Marinov
342a00a6af lib-jitsi-meet M67 2018-06-26 01:36:50 -05:00
Lyubo Marinov
bf14e66142 react-native-webrtc M67 2018-06-26 00:08:58 -05:00
Saúl Ibarra Corretgé
c3f602b7b6 Revert "fix(android): do not require java 8 target"
This reverts commit 9e0fee6c7d.

WebRTC requires Java 8, and Java 7 is now considered unsupported:
https://groups.google.com/forum/?utm_medium=email&utm_source=footer#!msg/discuss-webrtc/V1h2uQMDCkA/RA-uzncVAAAJ
2018-06-25 22:58:26 -05:00
virtuacoplenny
9a06d2bf52 ref(video-layout): consolidate connection status update handling (#3185)
- Instead of having 4 listeners for local connection status
  updates and 1 for remote, remove two of the redundant listeners.
- Instead of calling into 4 separate VideoLayout methods to update a
  participant's connection status, expose one handler.
2018-06-25 10:44:12 -07:00
virtuacoplenny
2951fefef9 ref(toolbar): change tooltip prop name to stop deprecation warning (#3186) 2018-06-25 09:31:41 -07:00
damencho
361e5f0fad Adds identification of poltergeist's in presence. 2018-06-22 18:23:17 -05:00
damencho
682169e44c Renames isBot -> isFakeParticipant. 2018-06-22 18:23:17 -05:00
Leonard Kim
c65ccb0af5 fix(filmstrip): do not show video for large video speaker in audio only
When in audio only, the video should not be shown and instead the
avatar should display.
2018-06-22 17:03:25 -05:00
Leonard Kim
f035861617 ref(toolbar): allow OverflowMenuItem to show beta tag 2018-06-22 15:53:35 -05:00
Boris Grozev
045dad3354 doc: Adds a link to the quick install tutorial. 2018-06-22 14:52:17 -05:00
Zoltan Bettenbuk
b5e9c71865 Merge pull request #3160 from saghul/mobile-sounds-loop
Mobile looping sounds
2018-06-22 17:48:13 +02:00
Saúl Ibarra Corretgé
7b0a6a2ee5 [RN] Add ability to loop sounds 2018-06-22 14:34:01 +02:00
Saúl Ibarra Corretgé
333a0f5f90 [RN] Handle presence
Up until now, mobile was oblivious to participants' presence state. Presence
state handling is required (probably, amongst other things) for "call flows".
So, let's add it! This is done by gathering the presence state when a
participant first joins, and handling subsequent changes.
2018-06-22 12:22:12 +02:00
Saúl Ibarra Corretgé
47aa14e9f6 [iOS] Fix syncing muted state with CallKit
Fix the "mute ping pong" for once and for all. This patch takes a new approach
to the problem: it keeps track of the user generated CallKit transaction ations
and avoids calling the delegate method in those cases.

This results in a much cleaner and easier to understand handling of the flow: if
the delegate method is called it means the user tapped on the mute button. When
we sync the muted state in JS with CallKit the delegate method won't be called
at all, thus avoiding the ping-pong altogether.

In addition, make sure all CallKit methods run in the UI thread. CallKit will
call our delegate methods in the UI thread too, thsu there is no need to
synchronize access to the listener / pending action sets.
2018-06-22 11:25:09 +02:00
Saúl Ibarra Corretgé
ec8ad6190d [iOS] Only update the matching local track data in CallKit 2018-06-22 11:25:09 +02:00
Saúl Ibarra Corretgé
42b85f73bd [iOS] Fix checking if a track is local in ithe CallKit middleware
Not all TRACK_ actions include the `local` attribute, so use the underlying
`jitsiTrack` to check it.
2018-06-22 11:25:09 +02:00
virtuacoplenny
2bd0f77671 Move a couple calls to update VideoLayout into the redux update flow (#3173)
* ref(video-layout): move middleware for TRACK_ADDED

* ref(video-layout): call mucJoined when redux knowns of conference join
2018-06-21 21:33:33 -07:00
bbaldino
11c9d5f0ef change the levels for each gsm bar color (#3174)
old values: 0-39% -> red, 40-69% -> yellow, 70%+ -> green
new values: 0-9% -> red, 10-29% -> yellow, 30%+ -> green
2018-06-21 17:30:19 -07:00
virtuacoplenny
942d7d7e56 chore(deps): update lib for recording refactoring and no display name escaping (#3169) 2018-06-21 15:47:25 -07:00
Leonard Kim
e431acda18 ref(css): add height/width reset for html tag
This is needed for newer versions of electron that might
use the iframe integration of jitsi-meet. Newer versions
seem to have some kind of regression with setting height
and width to 100%.
2018-06-21 12:39:55 -05:00
virtuacoplenny
7ee63a44c5 ref(info): use conference existence as trigger for autoshowing dialog (#3083)
* ref(info): use conference existence as trigger for autoshowing dialog

* squash: combine maybeShow checks, inheritdoc

* squash: flow type tweaks
2018-06-21 12:38:53 -05:00
Saúl Ibarra Corretgé
e3bc333115 Merge pull request #3163 from manquer/patch-1
grammar and spelling fixes
2018-06-21 09:16:08 +02:00
Manquer
750d6cf534 grammer and spelling fixes 2018-06-21 11:36:16 +05:30
Leonard Kim
fecd138a3c fix(recording): red error text for google api errors 2018-06-20 23:09:43 -05:00
Hristo Terezov
1f8fa3b6d4 Refactor settings modal (#3121)
* feat(settings): setting dialog

- Move device selection, profile edit, language select, moderator
  options, and server auth into one modal with tabs.
- Remove side panel profile and settings and logic used to update
  them.
- Pipe server auth status into redux to display in the settings
  dialog.
- Change filmstrip only device selection popup to use the new
  stateless settings dialog component.

* squash: do not show profile tab if not guest

* squash: profile button not clickable if no profile to show

* squash: nits

* ref: Settings dialog.
2018-06-20 13:19:53 -07:00
Jacob MacElroy
0acc9187ed Preventing expired notification for poltergeist that have left.
The original presence stanza generation code for a poltergeist
has been re-factored and simplified a bit. Every time a
poltergeist presence is updated we first check that the poltergeist
still exists.
2018-06-20 14:37:58 -05:00
paweldomas
675eea7b99 fix(base/conference): do not execute leave conference on web
On web CONFERENCE_FAILED handlers are not setting the 'recoverable'
flag thus any middleware which rely on those should not execute on web.
2018-06-20 10:58:57 -05:00
Saúl Ibarra Corretgé
146ffb0918 Merge pull request #3153 from jitsi/connection_corner_cases
Connection corner cases
2018-06-20 16:53:48 +02:00
paweldomas
57b302da3e feat(base/conference): CONFERENCE_FAILED on CONNECTION_FAILED
Emits CONFERENCE_FAILED in response to CONNECTION_FAILED event
which then triggers JitsiConference.leave() through the middleware
processing. Also base/conference state will be adjusted. It is to have
a consistent redux state in which both connection and conference are
failed. It could happen that in a buggy environment the XMPP connection
is dropped, but the media is still flowing which would result in weird
user experience.
2018-06-20 15:52:46 +02:00
paweldomas
b2f76f3ed6 ref(mobile/external-api): skip event if conference exists
The change to mobile/external-api is required to not emit
CONFERENCE_FAILED for CONNECTION_FAILED if the conference has been
started, because base/conference state will still hold conference
instances which are to be ended by other means and result in the
appropriate event (which will adjust the base/conference state).
2018-06-20 15:52:46 +02:00
paweldomas
85fbaac9b2 chore(deps): update LJM to 1bd6ee4b1207e6fd57f5b7c15cc17e0a6087f26d
Updates lib-jitsi-meet to have the getConnection getter in
JitsiConference.
2018-06-20 15:52:45 +02:00
paweldomas
022954b40b fix(base/connection/reducer): clear 'connection' field
Currently the listeners for disconnected and failed connection events
are unsubscribed as soon as the connection is established, so
the CONNECTION_DISCONNECTED is never triggered which would clear the
'connection' field. This commit will clear the 'connection' state on
CONNECTION_WILL_CONNECT. It's needed anyway given that there's no
guarantee on when and if the async disconnect operation will finish.

One issue caused by the 'connection' not cleared was that
CONNECTION_FAILED was not reduced correctly and the reload screen was
not displayed for the following scenario:
1. Join and leave any working conference.
2. Turn off network connectivity on the device.
3. Wait for CONNECTION_FAILED. The reload screen will not be displayed,
   because CONNECTION_FAILED is not reduced correctly, because the old
   'connection' value is still there.
2018-06-20 15:52:45 +02:00
Saúl Ibarra Corretgé
7fa941cb8c [iOS] Fix setting call type in CallKit
Your truly introduced this regression in
8c7a3f16b1, alas.

The audio only mode is used to set the CallKit call type. This affects the
behavior on the recent calls entries (calls are marked either as audio or video
calls).

Sync both at the start and for transitions. The previous code was working by
chance (in a way): when the CallKit UI is presented the local video is muted,
which triggers a SET_VIDEO_MUTED action, at which point the audio-only mode was
checked for. Now we are more explicit and act on SET_AUDIO_MUTED.
2018-06-20 08:42:30 -05:00
Daniel Ornelas
ad259988b9 [RN] Fix for creating video track when conference is ending. 2018-06-20 11:24:07 +02:00
Saúl Ibarra Corretgé
8c7a3f16b1 [iOS] Refactor muted state handling in CallKit
Rely solely on actual track state, rather than the desired state, (what
base/media represents).
2018-06-19 15:53:43 +02:00
Saúl Ibarra Corretgé
84c1c3dfd3 [iOS] Fix starting a call muted when permission was not granted
Read the muted state from the track itself instead of from base/media. This
avoid expressing the incorrect desire when the call starts muted because
permission was never granted.
2018-06-19 15:53:43 +02:00
hristoterezov
fccd0d6b29 ref(deep-linking): Improve the window loaded detection logic. 2018-06-18 18:01:22 -05:00
hristoterezov
12dda7acb9 fix(deep-linking): GUM when the deep linking page have been displayed. 2018-06-18 18:01:22 -05:00
Saúl Ibarra Corretgé
28861c0054 [iOS] Fix incorrect call to setAudioMuted in CallKit
Audio muting does not have an authority.
2018-06-18 15:45:37 -05:00
Lyubo Marinov
0d3fac7c0f [RN] Change default WelcomeScreen tab and persist user choice (coding style) 2018-06-18 15:42:09 -05:00
Bettenbuk Zoltan
dcfebf746f [RN] Change default WelcomeScreen tab and persist user choice 2018-06-18 12:21:55 -05:00
virtuacoplenny
4ab8d98cd1 ref(large-video): permanently enable canvas based background (#3084)
* ref(large-video): permanently enable canvas based background

* squash: leave flag for disabling background
2018-06-15 16:41:37 -05:00
472 changed files with 17217 additions and 12184 deletions

View File

@@ -23,6 +23,7 @@
; seen to cause errors and we have chosen not to fix.
.*/node_modules/@atlaskit/.*/*.js.flow
.*/node_modules/react-native-keep-awake/.*
.*/node_modules/react-native-permissions/.*
.*/node_modules/styled-components/.*
.*/\.git/.*

View File

@@ -1,4 +1,4 @@
osx_image: xcode9.3
osx_image: xcode9.4
language: objective-c
script:
- "./ios/travis-ci/build-ipa.sh"

View File

@@ -10,7 +10,7 @@ Jitsi Meet allows for very efficient collaboration. It allows users to stream th
On the client side, no installation is necessary. You just point your browser to the URL of your deployment. This section is about installing the Jitsi Meet suite on your server and hosting your own conferencing service.
Installing Jitsi Meet is quite a simple experience. For Debian-based systems, we recommend following the [quick-install](https://github.com/jitsi/jitsi-meet/blob/master/doc/quick-install.md) document, which uses the package system.
Installing Jitsi Meet is quite a simple experience. For Debian-based systems, we recommend following the [quick-install](https://github.com/jitsi/jitsi-meet/blob/master/doc/quick-install.md) document, which uses the package system. You can also see a demonstration of the process in [this tutorial video](https://jitsi.org/tutorial).
For other systems, or if you wish to install all components manually, see the [detailed manual installation instructions](https://github.com/jitsi/jitsi-meet/blob/master/doc/manual-install.md).

View File

@@ -42,15 +42,15 @@ dependencies {
cd android/
./gradlew :sdk:assembleRelease
```
When this successfully executes, artifacts/binaries are ready to be published
When this successfully executes, artifacts/binaries are ready to be published
into a Maven repository of your choice.
3. Configure the Maven repositories in which you are going to publish the
artifacts/binaries during step 4.
artifacts/binaries during step 4.
In the file `android/sdk/build.gradle` modify the line that contains
`"file:${rootProject.projectDir}/../../../jitsi/jitsi-maven-repository/releases"`
Change this value (which represents the Maven repository location used internally
by the Jitsi Developers) to the location of the repository that you'd like to use.
@@ -64,7 +64,7 @@ dependencies {
5. In _your_ project, add the Maven repository that you configured in step 3, as well
as the dependency `org.jitsi.react:jitsi-meet-sdk` into your `build.gradle`
file. Note that it's needed to pull in the transitive dependencies:
```gradle
implementation ('org.jitsi.react:jitsi-meet-sdk:+') { transitive = true }
```
@@ -76,18 +76,18 @@ repositories) continue below.
6. Create the release assembly for _each_ third-party react-native module that you
need, replacing it's name in the example below.
```bash
./gradlew :react-native-webrtc:assembleRelease
```
7. Configure the Maven repositories in which you are going to publish the
artifacts/binaries during step 8.
artifacts/binaries during step 8.
In the file `android/build.gradle` (note that this is a different file than the file
that was modified in step 3) modify the line that contains
`"file:${rootProject.projectDir}/../../../jitsi/jitsi-maven-repository/releases"`
Change this value (which represents the Maven repository location used internally
by the Jitsi Developers) to the location of the repository that you'd like to use.
You can use the same repository as the one you configured in step 3 if you want.
@@ -106,10 +106,21 @@ repositories) continue below.
## Using the API
=======
Jitsi Meet SDK is an Android library which embodies the whole Jitsi Meet
experience and makes it reusable by third-party apps.
First, add Java 1.8 compatibility support to your project by adding the
following lines into your `build.gradle` file:
```
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
```
To get started, extends your `android.app.Activity` from
`org.jitsi.meet.sdk.JitsiMeetActivity`:
@@ -194,13 +205,13 @@ which displays a single `JitsiMeetView`.
See JitsiMeetView.getDefaultURL.
#### getPictureInPictureEnabled()
#### isPictureInPictureEnabled()
See JitsiMeetView.getPictureInPictureEnabled.
See JitsiMeetView.isPictureInPictureEnabled.
#### getWelcomePageEnabled()
#### isWelcomePageEnabled()
See JitsiMeetView.getWelcomePageEnabled.
See JitsiMeetView.isWelcomePageEnabled.
#### loadURL(URL)
@@ -239,13 +250,13 @@ if set to `null`, the default built in JavaScript is used: https://meet.jit.si.
Returns the `JitsiMeetViewListener` instance attached to the view.
#### getPictureInPictureEnabled()
#### isPictureInPictureEnabled()
Returns `true` if Picture-in-Picture is enabled; `false`, otherwise. If not
explicitly set (by a preceding `setPictureInPictureEnabled` call), defaults to
`true` if the platform supports Picture-in-Picture natively; `false`, otherwise.
#### getWelcomePageEnabled()
#### isWelcomePageEnabled()
Returns true if the Welcome page is enabled; otherwise, false. If false, a black
empty view will be rendered when not in a conference. Defaults to false.
@@ -305,7 +316,7 @@ effect.
#### setWelcomePageEnabled(boolean)
Sets whether the Welcome page is enabled. See `getWelcomePageEnabled` for more
Sets whether the Welcome page is enabled. See `isWelcomePageEnabled` for more
information.
NOTE: Must be called (if at all) before `loadURL`/`loadURLString` for it to take

View File

@@ -25,12 +25,18 @@ android {
exclude '/lib/x86_64/**'
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
dependencies {

View File

@@ -68,3 +68,9 @@
-dontwarn java.nio.file.*
-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement
-dontwarn okio.**
# FastImage
-keep public class com.dylanvann.fastimage.* {*;}
-keep public class com.dylanvann.fastimage.** {*;}

View File

@@ -18,6 +18,7 @@ allprojects {
repositories {
google()
jcenter()
maven { url "$rootDir/../node_modules/jsc-android/dist" }
// React Native (JS, Obj-C sources, Android binaries) is installed from
// npm.
maven { url "$rootDir/../node_modules/react-native/android" }
@@ -34,6 +35,12 @@ allprojects {
def version = new groovy.json.JsonSlurper().parseText(file.text).version
details.useVersion version
}
if (details.requested.group == 'org.webkit'
&& details.requested.name == 'android-jsc') {
def file = new File("$rootDir/../node_modules/jsc-android/package.json")
def version = new groovy.json.JsonSlurper().parseText(file.text).version
details.useVersion "r${version.tokenize('.')[0]}"
}
}
}
}
@@ -146,7 +153,7 @@ allprojects {
ext {
buildToolsVersion = "26.0.2"
compileSdkVersion = 26
minSdkVersion = 16
minSdkVersion = 21
targetSdkVersion = 26
// The Maven artifact groupdId of the third-party react-native modules which

View File

@@ -25,9 +25,10 @@ dependencies {
compile 'com.facebook.react:react-native:+'
compile project(':react-native-background-timer')
compile project(':react-native-fetch-blob')
compile project(':react-native-fast-image')
compile project(':react-native-immersive')
compile project(':react-native-keep-awake')
compile project(':react-native-linear-gradient')
compile project(':react-native-locale-detector')
compile project(':react-native-sound')
compile project(':react-native-vector-icons')

View File

@@ -16,7 +16,9 @@ import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
class AndroidSettingsModule extends ReactContextBaseJavaModule {
class AndroidSettingsModule
extends ReactContextBaseJavaModule {
public AndroidSettingsModule(ReactApplicationContext reactContext) {
super(reactContext);
}

View File

@@ -1,3 +1,19 @@
/*
* Copyright @ 2017-present Atlassian Pty 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.
*/
package org.jitsi.meet.sdk;
import android.content.Context;
@@ -11,7 +27,9 @@ import com.facebook.react.bridge.ReactContextBaseJavaModule;
import java.util.HashMap;
import java.util.Map;
class AppInfoModule extends ReactContextBaseJavaModule {
class AppInfoModule
extends ReactContextBaseJavaModule {
public AppInfoModule(ReactApplicationContext reactContext) {
super(reactContext);
}

View File

@@ -25,8 +25,6 @@ import android.content.pm.PackageManager;
import android.media.AudioDeviceInfo;
import android.media.AudioManager;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import com.facebook.react.bridge.Arguments;
@@ -41,6 +39,8 @@ import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* Module implementing a simple API to select the appropriate audio device for a
@@ -57,7 +57,10 @@ import java.util.Set;
* Before a call has started and after it has ended the
* {@code AudioModeModule.DEFAULT} mode should be used.
*/
class AudioModeModule extends ReactContextBaseJavaModule implements AudioManager.OnAudioFocusChangeListener {
class AudioModeModule
extends ReactContextBaseJavaModule
implements AudioManager.OnAudioFocusChangeListener {
/**
* Constants representing the audio mode.
* - DEFAULT: Used before and after every call. It represents the default
@@ -115,10 +118,11 @@ class AudioModeModule extends ReactContextBaseJavaModule implements AudioManager
private BluetoothHeadsetMonitor bluetoothHeadsetMonitor;
/**
* {@link Handler} for running all operations on the main thread.
* {@link ExecutorService} for running all audio operations on a dedicated
* thread.
*/
private final Handler mainThreadHandler
= new Handler(Looper.getMainLooper());
private static final ExecutorService executor
= Executors.newSingleThreadExecutor();
/**
* {@link Runnable} for running audio device detection the main thread.
@@ -225,7 +229,7 @@ class AudioModeModule extends ReactContextBaseJavaModule implements AudioManager
// Do an initial detection on Android >= M.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
mainThreadHandler.post(onAudioDeviceChangeRunner);
runInAudioThread(onAudioDeviceChangeRunner);
} else {
// On Android < M, detect if we have an earpiece.
PackageManager pm = reactContext.getPackageManager();
@@ -265,7 +269,7 @@ class AudioModeModule extends ReactContextBaseJavaModule implements AudioManager
*/
@ReactMethod
public void getAudioDevices(final Promise promise) {
mainThreadHandler.post(new Runnable() {
runInAudioThread(new Runnable() {
@Override
public void run() {
WritableMap map = Arguments.createMap();
@@ -302,7 +306,7 @@ class AudioModeModule extends ReactContextBaseJavaModule implements AudioManager
* Only used on Android >= M.
*/
void onAudioDeviceChange() {
mainThreadHandler.post(onAudioDeviceChangeRunner);
runInAudioThread(onAudioDeviceChangeRunner);
}
/**
@@ -330,7 +334,7 @@ class AudioModeModule extends ReactContextBaseJavaModule implements AudioManager
* Only used on Android < M.
*/
void onHeadsetDeviceChange() {
mainThreadHandler.post(new Runnable() {
runInAudioThread(new Runnable() {
@Override
public void run() {
// XXX: isWiredHeadsetOn is not deprecated when used just for
@@ -380,6 +384,14 @@ class AudioModeModule extends ReactContextBaseJavaModule implements AudioManager
}
}
/**
* Helper function to run operations on a dedicated thread.
* @param runnable
*/
public void runInAudioThread(Runnable runnable) {
executor.execute(runnable);
}
/**
* Sets the user selected audio device as the active audio device.
*
@@ -387,7 +399,7 @@ class AudioModeModule extends ReactContextBaseJavaModule implements AudioManager
*/
@ReactMethod
public void setAudioDevice(final String device) {
mainThreadHandler.post(new Runnable() {
runInAudioThread(new Runnable() {
@Override
public void run() {
if (!availableDevices.contains(device)) {
@@ -458,7 +470,7 @@ class AudioModeModule extends ReactContextBaseJavaModule implements AudioManager
}
}
};
mainThreadHandler.post(r);
runInAudioThread(r);
}
/**

View File

@@ -0,0 +1,231 @@
/*
* Copyright @ 2018-present Atlassian Pty 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.
*/
package org.jitsi.meet.sdk;
import android.app.Activity;
import android.content.Context;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;
import android.widget.FrameLayout;
import com.facebook.react.ReactRootView;
import com.facebook.react.bridge.ReadableMap;
import com.rnimmersive.RNImmersiveModule;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.WeakHashMap;
/**
* Base class for all views which are backed by a React Native view.
*/
public abstract class BaseReactView<ListenerT>
extends FrameLayout {
/**
* Background color used by {@code BaseReactView} and the React Native root
* view.
*/
protected static int BACKGROUND_COLOR = 0xFF111111;
/**
* The collection of all existing {@code BaseReactView}s. Used to find the
* {@code BaseReactView} when delivering events coming from
* {@link ExternalAPIModule}.
*/
static final Set<BaseReactView> views
= Collections.newSetFromMap(new WeakHashMap<BaseReactView, Boolean>());
/**
* Finds a {@code BaseReactView} which matches a specific external API
* scope.
*
* @param externalAPIScope - The external API scope associated with the
* {@code BaseReactView} to find.
* @return The {@code BaseReactView}, if any, associated with the specified
* {@code externalAPIScope}; otherwise, {@code null}.
*/
public static BaseReactView findViewByExternalAPIScope(
String externalAPIScope) {
synchronized (views) {
for (BaseReactView view : views) {
if (view.externalAPIScope.equals(externalAPIScope)) {
return view;
}
}
}
return null;
}
/**
* The unique identifier of this {@code BaseReactView} within the process
* for the purposes of {@link ExternalAPIModule}. The name scope was
* inspired by postis which we use on Web for the similar purposes of the
* iframe-based external API.
*/
protected final String externalAPIScope;
/**
* The listener (e.g. {@link JitsiMeetViewListener}) instance for reporting
* events occurring in Jitsi Meet.
*/
private ListenerT listener;
/**
* React Native root view.
*/
private ReactRootView reactRootView;
public BaseReactView(@NonNull Context context) {
super(context);
setBackgroundColor(BACKGROUND_COLOR);
ReactInstanceManagerHolder.initReactInstanceManager(
((Activity) context).getApplication());
// Hook this BaseReactView into ExternalAPI.
externalAPIScope = UUID.randomUUID().toString();
synchronized (views) {
views.add(this);
}
}
/**
* Creates the {@code ReactRootView} for the given app name with the given
* props. Once created it's set as the view of this {@code FrameLayout}.
*
* @param appName - The name of the "app" (in React Native terms) to load.
* @param props - The React Component props to pass to the app.
*/
public void createReactRootView(String appName, @Nullable Bundle props) {
if (props == null) {
props = new Bundle();
}
props.putString("externalAPIScope", externalAPIScope);
if (reactRootView == null) {
reactRootView = new ReactRootView(getContext());
reactRootView.startReactApplication(
ReactInstanceManagerHolder.getReactInstanceManager(),
appName,
props);
reactRootView.setBackgroundColor(BACKGROUND_COLOR);
addView(reactRootView);
} else {
reactRootView.setAppProperties(props);
}
}
/**
* Releases the React resources (specifically the {@link ReactRootView})
* associated with this view.
*
* MUST be called when the {@link Activity} holding this view is destroyed,
* typically in the {@code onDestroy} method.
*/
public void dispose() {
if (reactRootView != null) {
removeView(reactRootView);
reactRootView.unmountReactApplication();
reactRootView = null;
}
}
/**
* Gets the listener set on this {@code BaseReactView}.
*
* @return The listener set on this {@code BaseReactView}.
*/
public ListenerT getListener() {
return listener;
}
/**
* Abstract method called by {@link ExternalAPIModule} when an event is
* received for this view.
*
* @param name - The name of the event.
* @param data - The details of the event associated with/specific to the
* specified {@code name}.
*/
public abstract void onExternalAPIEvent(String name, ReadableMap data);
protected void onExternalAPIEvent(
Map<String, Method> listenerMethods,
String name, ReadableMap data) {
ListenerT listener = getListener();
if (listener != null) {
ListenerUtils.runListenerMethod(
listener, listenerMethods, name, data);
}
}
/**
* Called when the window containing this view gains or loses focus.
*
* @param hasFocus If the window of this view now has focus, {@code true};
* otherwise, {@code false}.
*/
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
// https://github.com/mockingbot/react-native-immersive#restore-immersive-state
// FIXME The singleton pattern employed by RNImmersiveModule is not
// advisable because a react-native mobule is consumable only after its
// BaseJavaModule#initialize() has completed and here we have no
// knowledge of whether the precondition is really met.
RNImmersiveModule immersive = RNImmersiveModule.getInstance();
if (hasFocus && immersive != null) {
try {
immersive.emitImmersiveStateChangeEvent();
} catch (RuntimeException re) {
// FIXME I don't know how to check myself whether
// BaseJavaModule#initialize() has been invoked and thus
// RNImmersiveModule is consumable. A safe workaround is to
// swallow the failure because the whole full-screen/immersive
// functionality is brittle anyway, akin to the icing on the
// cake, and has been working without onWindowFocusChanged for a
// very long time.
Log.e(
"RNImmersiveModule",
"emitImmersiveStateChangeEvent() failed!",
re);
}
}
}
/**
* Sets a specific listener on this {@code BaseReactView}.
*
* @param listener The listener to set on this {@code BaseReactView}.
*/
public void setListener(ListenerT listener) {
this.listener = listener;
}
}

View File

@@ -24,8 +24,6 @@ import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.media.AudioManager;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
/**
@@ -55,12 +53,6 @@ class BluetoothHeadsetMonitor {
*/
private boolean headsetAvailable = false;
/**
* {@link Handler} for running all operations on the main thread.
*/
private final Handler mainThreadHandler
= new Handler(Looper.getMainLooper());
/**
* Helper for running Bluetooth operations on the main thread.
*/
@@ -200,6 +192,6 @@ class BluetoothHeadsetMonitor {
* {@link AudioModeModule#onAudioDeviceChange()} callback.
*/
private void updateDevices() {
mainThreadHandler.post(updateDevicesRunnable);
audioModeModule.runInAudioThread(updateDevicesRunnable);
}
}

View File

@@ -16,81 +16,24 @@
package org.jitsi.meet.sdk;
import android.util.Log;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.ReadableMapKeySetIterator;
import com.facebook.react.bridge.UiThreadUtil;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Pattern;
/**
* Module implementing a simple API to enable a proximity sensor-controlled
* wake lock. When the lock is held, if the proximity sensor detects a nearby
* object it will dim the screen and disable touch controls. The functionality
* is used with the conference audio-only mode.
* Module implementing an API for sending events from JavaScript to native code.
*/
class ExternalAPIModule extends ReactContextBaseJavaModule {
/**
* The {@code Method}s of {@code JitsiMeetViewListener} by event name i.e.
* redux action types.
*/
private static final Map<String, Method> JITSI_MEET_VIEW_LISTENER_METHODS
= new HashMap<>();
class ExternalAPIModule
extends ReactContextBaseJavaModule {
static {
// Figure out the mapping between the JitsiMeetViewListener methods
// and the events i.e. redux action types.
Pattern onPattern = Pattern.compile("^on[A-Z]+");
Pattern camelcasePattern = Pattern.compile("([a-z0-9]+)([A-Z0-9]+)");
for (Method method : JitsiMeetViewListener.class.getDeclaredMethods()) {
// * The method must be public (because it is declared by an
// interface).
// * The method must be/return void.
if (!Modifier.isPublic(method.getModifiers())
|| !Void.TYPE.equals(method.getReturnType())) {
continue;
}
// * The method name must start with "on" followed by a
// capital/uppercase letter (in agreement with the camelcase
// coding style customary to Java in general and the projects of
// the Jitsi community in particular).
String name = method.getName();
if (!onPattern.matcher(name).find()) {
continue;
}
// * The method must accept/have exactly 1 parameter of a type
// assignable from HashMap.
Class<?>[] parameterTypes = method.getParameterTypes();
if (parameterTypes.length != 1
|| !parameterTypes[0].isAssignableFrom(HashMap.class)) {
continue;
}
// Convert the method name to an event name.
name
= camelcasePattern.matcher(name.substring(2))
.replaceAll("$1_$2")
.toUpperCase(Locale.ROOT);
JITSI_MEET_VIEW_LISTENER_METHODS.put(name, method);
}
}
private static final String TAG = ExternalAPIModule.class.getSimpleName();
/**
* Initializes a new module instance. There shall be a single instance of
* this module throughout the lifetime of the application.
* this module throughout the lifetime of the app.
*
* @param reactContext the {@link ReactApplicationContext} where this module
* is created.
@@ -109,39 +52,9 @@ class ExternalAPIModule extends ReactContextBaseJavaModule {
return "ExternalAPI";
}
/**
* The internal processing for the URL of the current conference set on the
* associated {@link JitsiMeetView}.
*
* @param eventName the name of the external API event to be processed
* @param eventData the details/specifics of the event to process determined
* by/associated with the specified {@code eventName}.
* @param view the {@link JitsiMeetView} instance.
*/
private void maybeSetViewURL(
String eventName,
ReadableMap eventData,
JitsiMeetView view) {
switch(eventName) {
case "CONFERENCE_WILL_JOIN":
view.setURL(eventData.getString("url"));
break;
case "CONFERENCE_FAILED":
case "CONFERENCE_WILL_LEAVE":
case "LOAD_CONFIG_ERROR":
String url = eventData.getString("url");
if (url != null && url.equals(view.getURL())) {
view.setURL(null);
}
break;
}
}
/**
* Dispatches an event that occurred on the JavaScript side of the SDK to
* the specified {@link JitsiMeetView}'s listener.
* the specified {@link BaseReactView}'s listener.
*
* @param name The name of the event.
* @param data The details/specifics of the event to send determined
@@ -149,106 +62,18 @@ class ExternalAPIModule extends ReactContextBaseJavaModule {
* @param scope
*/
@ReactMethod
public void sendEvent(final String name,
final ReadableMap data,
final String scope) {
public void sendEvent(String name, ReadableMap data, String scope) {
// The JavaScript App needs to provide uniquely identifying information
// to the native ExternalAPI module so that the latter may match the
// former to the native JitsiMeetView which hosts it.
JitsiMeetView view = JitsiMeetView.findViewByExternalAPIScope(scope);
// former to the native BaseReactView which hosts it.
BaseReactView view = BaseReactView.findViewByExternalAPIScope(scope);
if (view == null) {
return;
}
// XXX The JitsiMeetView property URL was introduced in order to address
// an exception in the Picture-in-Picture functionality which arose
// because of delays related to bridging between JavaScript and Java. To
// reduce these delays do not wait for the call to be transfered to the
// UI thread.
maybeSetViewURL(name, data, view);
// Make sure JitsiMeetView's listener is invoked on the UI thread. It
// was requested by SDK consumers.
if (UiThreadUtil.isOnUiThread()) {
sendEventOnUiThread(name, data, scope);
} else {
UiThreadUtil.runOnUiThread(new Runnable() {
@Override
public void run() {
sendEventOnUiThread(name, data, scope);
}
});
}
}
/**
* Dispatches an event that occurred on the JavaScript side of the SDK to
* the specified {@link JitsiMeetView}'s listener on the UI thread.
*
* @param name The name of the event.
* @param data The details/specifics of the event to send determined
* by/associated with the specified {@code name}.
* @param scope
*/
private void sendEventOnUiThread(final String name,
final ReadableMap data,
final String scope) {
// The JavaScript App needs to provide uniquely identifying information
// to the native ExternalAPI module so that the latter may match the
// former to the native JitsiMeetView which hosts it.
JitsiMeetView view = JitsiMeetView.findViewByExternalAPIScope(scope);
if (view == null) {
return;
}
JitsiMeetViewListener listener = view.getListener();
if (listener == null) {
return;
}
Method method = JITSI_MEET_VIEW_LISTENER_METHODS.get(name);
if (method != null) {
if (view != null) {
try {
method.invoke(listener, toHashMap(data));
} catch (IllegalAccessException e) {
// FIXME There was a multicatch for IllegalAccessException and
// InvocationTargetException, but Android Studio complained
// with: "Multi-catch with these reflection exceptions requires
// API level 19 (current min is 16) because they get compiled to
// the common but new super type ReflectiveOperationException.
// As a workaround either create individual catch statements, or
// catch Exception."
throw new RuntimeException(e);
} catch (InvocationTargetException e) {
throw new RuntimeException(e);
view.onExternalAPIEvent(name, data);
} catch(Exception e) {
Log.e(TAG, "onExternalAPIEvent: error sending event", e);
}
}
}
/**
* Initializes a new {@code HashMap} instance with the key-value
* associations of a specific {@code ReadableMap}.
*
* @param readableMap the {@code ReadableMap} specifying the key-value
* associations with which the new {@code HashMap} instance is to be
* initialized.
* @return a new {@code HashMap} instance initialized with the key-value
* associations of the specified {@code readableMap}.
*/
private HashMap<String, Object> toHashMap(ReadableMap readableMap) {
HashMap<String, Object> hashMap = new HashMap<>();
for (ReadableMapKeySetIterator i = readableMap.keySetIterator();
i.hasNextKey();) {
String key = i.nextKey();
hashMap.put(key, readableMap.getString(key));
}
return hashMap;
}
}

View File

@@ -41,7 +41,9 @@ import java.net.URL;
* hooked to the React Native subsystem via proxy calls through the
* {@code JitsiMeetView} static methods.
*/
public class JitsiMeetActivity extends AppCompatActivity {
public class JitsiMeetActivity
extends AppCompatActivity {
/**
* The request code identifying requests for the permission to draw on top
* of other apps. The value must be 16-bit and is arbitrarily chosen here.
@@ -95,25 +97,6 @@ public class JitsiMeetActivity extends AppCompatActivity {
return view == null ? defaultURL : view.getDefaultURL();
}
/**
*
* @see JitsiMeetView#getPictureInPictureEnabled()
*/
public boolean getPictureInPictureEnabled() {
return
view == null
? pictureInPictureEnabled
: view.getPictureInPictureEnabled();
}
/**
*
* @see JitsiMeetView#getWelcomePageEnabled()
*/
public boolean getWelcomePageEnabled() {
return view == null ? welcomePageEnabled : view.getWelcomePageEnabled();
}
/**
* Initializes the {@link #view} of this {@code JitsiMeetActivity} with a
* new {@link JitsiMeetView} instance.
@@ -152,6 +135,25 @@ public class JitsiMeetActivity extends AppCompatActivity {
return view;
}
/**
*
* @see JitsiMeetView#isPictureInPictureEnabled()
*/
public boolean isPictureInPictureEnabled() {
return
view == null
? pictureInPictureEnabled
: view.isPictureInPictureEnabled();
}
/**
*
* @see JitsiMeetView#isWelcomePageEnabled()
*/
public boolean isWelcomePageEnabled() {
return view == null ? welcomePageEnabled : view.isWelcomePageEnabled();
}
/**
* Loads the given URL and displays the conference. If the specified URL is
* null, the welcome page is displayed instead.
@@ -177,7 +179,7 @@ public class JitsiMeetActivity extends AppCompatActivity {
@Override
public void onBackPressed() {
if (!JitsiMeetView.onBackPressed()) {
if (!ReactActivityLifecycleCallbacks.onBackPressed()) {
// JitsiMeetView didn't handle the invocation of the back button.
// Generally, an Activity extender would very likely want to invoke
// Activity#onBackPressed(). For the sake of consistency with
@@ -220,7 +222,7 @@ public class JitsiMeetActivity extends AppCompatActivity {
view = null;
}
JitsiMeetView.onHostDestroy(this);
ReactActivityLifecycleCallbacks.onHostDestroy(this);
}
// ReactAndroid/src/main/java/com/facebook/react/ReactActivity.java
@@ -242,7 +244,20 @@ public class JitsiMeetActivity extends AppCompatActivity {
@Override
public void onNewIntent(Intent intent) {
JitsiMeetView.onNewIntent(intent);
// XXX At least twice we received bug reports about malfunctioning
// loadURL in the Jitsi Meet SDK while the Jitsi Meet app seemed to
// functioning as expected in our testing. But that was to be expected
// because the app does not exercise loadURL. In order to increase the
// test coverage of loadURL, channel deep linking through loadURL.
Uri uri;
if (Intent.ACTION_VIEW.equals(intent.getAction())
&& (uri = intent.getData()) != null
&& JitsiMeetView.loadURLStringInViews(uri.toString())) {
return;
}
ReactActivityLifecycleCallbacks.onNewIntent(intent);
}
@Override
@@ -250,21 +265,21 @@ public class JitsiMeetActivity extends AppCompatActivity {
super.onResume();
defaultBackButtonImpl = new DefaultHardwareBackBtnHandlerImpl(this);
JitsiMeetView.onHostResume(this, defaultBackButtonImpl);
ReactActivityLifecycleCallbacks.onHostResume(this, defaultBackButtonImpl);
}
@Override
public void onStop() {
super.onStop();
JitsiMeetView.onHostPause(this);
ReactActivityLifecycleCallbacks.onHostPause(this);
defaultBackButtonImpl = null;
}
@Override
protected void onUserLeaveHint() {
if (view != null) {
view.onUserLeaveHint();
view.enterPictureInPicture();
}
}

View File

@@ -16,57 +16,35 @@
package org.jitsi.meet.sdk;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;
import android.widget.FrameLayout;
import com.facebook.react.ReactInstanceManager;
import com.facebook.react.ReactRootView;
import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler;
import com.rnimmersive.RNImmersiveModule;
import com.facebook.react.bridge.ReadableMap;
import org.jitsi.meet.sdk.invite.InviteController;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.Collections;
import java.util.Set;
import java.util.UUID;
import java.util.WeakHashMap;
import java.util.Map;
public class JitsiMeetView
extends BaseReactView<JitsiMeetViewListener> {
public class JitsiMeetView extends FrameLayout {
/**
* Background color used by {@code JitsiMeetView} and the React Native root
* view.
* The {@code Method}s of {@code JitsiMeetViewListener} by event name i.e.
* redux action types.
*/
private static final int BACKGROUND_COLOR = 0xFF111111;
private static final Map<String, Method> LISTENER_METHODS
= ListenerUtils.mapListenerMethods(JitsiMeetViewListener.class);
/**
* The {@link Log} tag which identifies the source of the log messages of
* {@code JitsiMeetView}.
*/
private final static String TAG = JitsiMeetView.class.getSimpleName();
private static final Set<JitsiMeetView> views
= Collections.newSetFromMap(new WeakHashMap<JitsiMeetView, Boolean>());
public static JitsiMeetView findViewByExternalAPIScope(
String externalAPIScope) {
synchronized (views) {
for (JitsiMeetView view : views) {
if (view.externalAPIScope.equals(externalAPIScope)) {
return view;
}
}
}
return null;
}
private static final String TAG = JitsiMeetView.class.getSimpleName();
/**
* Loads a specific URL {@code String} in all existing
@@ -78,130 +56,19 @@ public class JitsiMeetView extends FrameLayout {
* at least one {@code JitsiMeetView}, then {@code true}; otherwise,
* {@code false}.
*/
private static boolean loadURLStringInViews(String urlString) {
synchronized (views) {
if (!views.isEmpty()) {
for (JitsiMeetView view : views) {
view.loadURLString(urlString);
}
public static boolean loadURLStringInViews(String urlString) {
boolean loaded = false;
return true;
synchronized (views) {
for (BaseReactView view : views) {
if (view instanceof JitsiMeetView) {
((JitsiMeetView)view).loadURLString(urlString);
loaded = true;
}
}
}
return false;
}
/**
* Activity lifecycle method which should be called from
* {@code Activity.onBackPressed} so we can do the required internal
* processing.
*
* @return {@code true} if the back-press was processed; {@code false},
* otherwise. If {@code false}, the application should call the parent's
* implementation.
*/
public static boolean onBackPressed() {
ReactInstanceManager reactInstanceManager
= ReactInstanceManagerHolder.getReactInstanceManager();
if (reactInstanceManager == null) {
return false;
} else {
reactInstanceManager.onBackPressed();
return true;
}
}
/**
* Activity lifecycle method which should be called from
* {@code Activity.onDestroy} so we can do the required internal
* processing.
*
* @param activity {@code Activity} being destroyed.
*/
public static void onHostDestroy(Activity activity) {
ReactInstanceManager reactInstanceManager
= ReactInstanceManagerHolder.getReactInstanceManager();
if (reactInstanceManager != null) {
reactInstanceManager.onHostDestroy(activity);
}
}
/**
* Activity lifecycle method which should be called from
* {@code Activity.onPause} so we can do the required internal processing.
*
* @param activity {@code Activity} being paused.
*/
public static void onHostPause(Activity activity) {
ReactInstanceManager reactInstanceManager
= ReactInstanceManagerHolder.getReactInstanceManager();
if (reactInstanceManager != null) {
reactInstanceManager.onHostPause(activity);
}
}
/**
* Activity lifecycle method which should be called from
* {@code Activity.onResume} so we can do the required internal processing.
*
* @param activity {@code Activity} being resumed.
*/
public static void onHostResume(Activity activity) {
onHostResume(activity, new DefaultHardwareBackBtnHandlerImpl(activity));
}
/**
* Activity lifecycle method which should be called from
* {@code Activity.onResume} so we can do the required internal processing.
*
* @param activity {@code Activity} being resumed.
* @param defaultBackButtonImpl a {@code DefaultHardwareBackBtnHandler} to
* handle invoking the back button if no {@code JitsiMeetView} handles it.
*/
public static void onHostResume(
Activity activity,
DefaultHardwareBackBtnHandler defaultBackButtonImpl) {
ReactInstanceManager reactInstanceManager
= ReactInstanceManagerHolder.getReactInstanceManager();
if (reactInstanceManager != null) {
reactInstanceManager.onHostResume(activity, defaultBackButtonImpl);
}
}
/**
* Activity lifecycle method which should be called from
* {@code Activity.onNewIntent} so we can do the required internal
* processing. Note that this is only needed if the activity's "launchMode"
* was set to "singleTask". This is required for deep linking to work once
* the application is already running.
*
* @param intent {@code Intent} instance which was received.
*/
public static void onNewIntent(Intent intent) {
// XXX At least twice we received bug reports about malfunctioning
// loadURL in the Jitsi Meet SDK while the Jitsi Meet app seemed to
// functioning as expected in our testing. But that was to be expected
// because the app does not exercise loadURL. In order to increase the
// test coverage of loadURL, channel deep linking through loadURL.
Uri uri;
if (Intent.ACTION_VIEW.equals(intent.getAction())
&& (uri = intent.getData()) != null
&& loadURLStringInViews(uri.toString())) {
return;
}
ReactInstanceManager reactInstanceManager
= ReactInstanceManagerHolder.getReactInstanceManager();
if (reactInstanceManager != null) {
reactInstanceManager.onNewIntent(intent);
}
return loaded;
}
/**
@@ -211,26 +78,12 @@ public class JitsiMeetView extends FrameLayout {
*/
private URL defaultURL;
/**
* The unique identifier of this {@code JitsiMeetView} within the process
* for the purposes of {@link ExternalAPI}. The name scope was inspired by
* postis which we use on Web for the similar purposes of the iframe-based
* external API.
*/
private final String externalAPIScope;
/**
* The entry point into the invite feature of Jitsi Meet. The Java
* counterpart of the JavaScript {@code InviteButton}.
*/
private final InviteController inviteController;
/**
* {@link JitsiMeetViewListener} instance for reporting events occurring in
* Jitsi Meet.
*/
private JitsiMeetViewListener listener;
/**
* Whether Picture-in-Picture is enabled. If {@code null}, defaults to
* {@code true} iff the Android platform supports Picture-in-Picture
@@ -238,11 +91,6 @@ public class JitsiMeetView extends FrameLayout {
*/
private Boolean pictureInPictureEnabled;
/**
* React Native root view.
*/
private ReactRootView reactRootView;
/**
* The URL of the current conference.
*/
@@ -258,34 +106,33 @@ public class JitsiMeetView extends FrameLayout {
public JitsiMeetView(@NonNull Context context) {
super(context);
setBackgroundColor(BACKGROUND_COLOR);
ReactInstanceManagerHolder.initReactInstanceManager(
((Activity) context).getApplication());
// Hook this JitsiMeetView into ExternalAPI.
externalAPIScope = UUID.randomUUID().toString();
synchronized (views) {
views.add(this);
}
// The entry point into the invite feature of Jitsi Meet. The Java
// counterpart of the JavaScript InviteButton.
inviteController = new InviteController(externalAPIScope);
}
/**
* Releases the React resources (specifically the {@link ReactRootView})
* associated with this view.
* Enters Picture-In-Picture mode, if possible. This method is designed to
* be called from the {@code Activity.onUserLeaveHint} method.
*
* This method MUST be called when the Activity holding this view is
* destroyed, typically in the {@code onDestroy} method.
* This is currently not mandatory, but if used will provide automatic
* handling of the picture in picture mode when user minimizes the app. It
* will be probably the most useful in case the app is using the welcome
* page.
*/
public void dispose() {
if (reactRootView != null) {
removeView(reactRootView);
reactRootView.unmountReactApplication();
reactRootView = null;
public void enterPictureInPicture() {
if (isPictureInPictureEnabled() && getURL() != null) {
PictureInPictureModule pipModule
= ReactInstanceManagerHolder.getNativeModule(
PictureInPictureModule.class);
if (pipModule != null) {
try {
pipModule.enterPictureInPicture();
} catch (RuntimeException re) {
Log.e(TAG, "onUserLeaveHint: failed to enter PiP mode", re);
}
}
}
}
@@ -294,7 +141,7 @@ public class JitsiMeetView extends FrameLayout {
* partial URL (e.g. a room name only) is specified to
* {@link #loadURLString(String)} or {@link #loadURLObject(Bundle)}. If not
* set or if set to {@code null}, the default built in JavaScript is used:
* {@link https://meet.jit.si}
* https://meet.jit.si
*
* @return The default base {@code URL} or {@code null}.
*/
@@ -315,31 +162,6 @@ public class JitsiMeetView extends FrameLayout {
return inviteController;
}
/**
* Gets the {@link JitsiMeetViewListener} set on this {@code JitsiMeetView}.
*
* @return The {@code JitsiMeetViewListener} set on this
* {@code JitsiMeetView}.
*/
public JitsiMeetViewListener getListener() {
return listener;
}
/**
* Gets whether Picture-in-Picture is enabled. Picture-in-Picture is
* natively supported on Android API >= 26 (Oreo), so it should not be
* enabled on older platform versions.
*
* @return If Picture-in-Picture is enabled, {@code true}; {@code false},
* otherwise.
*/
public boolean getPictureInPictureEnabled() {
return
PictureInPictureModule.isPictureInPictureSupported()
&& (pictureInPictureEnabled == null
|| pictureInPictureEnabled.booleanValue());
}
/**
* Gets the URL of the current conference.
*
@@ -353,6 +175,21 @@ public class JitsiMeetView extends FrameLayout {
return url;
}
/**
* Gets whether Picture-in-Picture is enabled. Picture-in-Picture is
* natively supported on Android API >= 26 (Oreo), so it should not be
* enabled on older platform versions.
*
* @return If Picture-in-Picture is enabled, {@code true}; {@code false},
* otherwise.
*/
public boolean isPictureInPictureEnabled() {
return
PictureInPictureModule.isPictureInPictureSupported()
&& (pictureInPictureEnabled == null
|| pictureInPictureEnabled);
}
/**
* Gets whether the Welcome page is enabled. If {@code true}, the Welcome
* page is rendered when this {@code JitsiMeetView} is not at a URL
@@ -361,7 +198,7 @@ public class JitsiMeetView extends FrameLayout {
* @return {@code true} if the Welcome page is enabled; otherwise,
* {@code false}.
*/
public boolean getWelcomePageEnabled() {
public boolean isWelcomePageEnabled() {
return welcomePageEnabled;
}
@@ -395,9 +232,6 @@ public class JitsiMeetView extends FrameLayout {
props.putString("defaultURL", defaultURL.toString());
}
// externalAPIScope
props.putString("externalAPIScope", externalAPIScope);
// inviteController
InviteController inviteController = getInviteController();
@@ -413,7 +247,7 @@ public class JitsiMeetView extends FrameLayout {
// pictureInPictureEnabled
props.putBoolean(
"pictureInPictureEnabled",
getPictureInPictureEnabled());
isPictureInPictureEnabled());
// url
if (urlObject != null) {
@@ -434,17 +268,7 @@ public class JitsiMeetView extends FrameLayout {
// per loadURLObject: invocation.
props.putLong("timestamp", System.currentTimeMillis());
if (reactRootView == null) {
reactRootView = new ReactRootView(getContext());
reactRootView.startReactApplication(
ReactInstanceManagerHolder.getReactInstanceManager(),
"App",
props);
reactRootView.setBackgroundColor(BACKGROUND_COLOR);
addView(reactRootView);
} else {
reactRootView.setAppProperties(props);
}
createReactRootView("App", props);
}
/**
@@ -468,66 +292,48 @@ public class JitsiMeetView extends FrameLayout {
}
/**
* Activity lifecycle method which should be called from
* {@code Activity.onUserLeaveHint} so we can do the required internal
* processing.
* The internal processing for the URL of the current conference set on the
* associated {@link JitsiMeetView}.
*
* This is currently not mandatory, but if used will provide automatic
* handling of the picture in picture mode when user minimizes the app. It
* will be probably the most useful in case the app is using the welcome
* page.
* @param eventName the name of the external API event to be processed
* @param eventData the details/specifics of the event to process determined
* by/associated with the specified {@code eventName}.
*/
public void onUserLeaveHint() {
if (getPictureInPictureEnabled() && getURL() != null) {
PictureInPictureModule pipModule
= ReactInstanceManagerHolder.getNativeModule(
PictureInPictureModule.class);
private void maybeSetViewURL(String eventName, ReadableMap eventData) {
switch(eventName) {
case "CONFERENCE_WILL_JOIN":
setURL(eventData.getString("url"));
break;
if (pipModule != null) {
try {
pipModule.enterPictureInPicture();
} catch (RuntimeException re) {
Log.e(TAG, "onUserLeaveHint: failed to enter PiP mode", re);
}
case "CONFERENCE_FAILED":
case "CONFERENCE_WILL_LEAVE":
case "LOAD_CONFIG_ERROR":
String url = eventData.getString("url");
if (url != null && url.equals(getURL())) {
setURL(null);
}
break;
}
}
/**
* Called when the window containing this view gains or loses focus.
* Handler for {@link ExternalAPIModule} events.
*
* @param hasFocus If the window of this view now has focus, {@code true};
* otherwise, {@code false}.
* @param name The name of the event.
* @param data The details/specifics of the event to send determined
* by/associated with the specified {@code name}.
*/
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
public void onExternalAPIEvent(String name, ReadableMap data) {
// XXX The JitsiMeetView property URL was introduced in order to address
// an exception in the Picture-in-Picture functionality which arose
// because of delays related to bridging between JavaScript and Java. To
// reduce these delays do not wait for the call to be transferred to the
// UI thread.
maybeSetViewURL(name, data);
// https://github.com/mockingbot/react-native-immersive#restore-immersive-state
// FIXME The singleton pattern employed by RNImmersiveModule is not
// advisable because a react-native mobule is consumable only after its
// BaseJavaModule#initialize() has completed and here we have no
// knowledge of whether the precondition is really met.
RNImmersiveModule immersive = RNImmersiveModule.getInstance();
if (hasFocus && immersive != null) {
try {
immersive.emitImmersiveStateChangeEvent();
} catch (RuntimeException re) {
// FIXME I don't know how to check myself whether
// BaseJavaModule#initialize() has been invoked and thus
// RNImmersiveModule is consumable. A safe workaround is to
// swallow the failure because the whole full-screen/immersive
// functionality is brittle anyway, akin to the icing on the
// cake, and has been working without onWindowFocusChanged for a
// very long time.
Log.e(
TAG,
"RNImmersiveModule#emitImmersiveStateChangeEvent() failed!",
re);
}
}
onExternalAPIEvent(LISTENER_METHODS, name, data);
}
/**
@@ -543,17 +349,6 @@ public class JitsiMeetView extends FrameLayout {
this.defaultURL = defaultURL;
}
/**
* Sets a specific {@link JitsiMeetViewListener} on this
* {@code JitsiMeetView}.
*
* @param listener The {@code JitsiMeetViewListener} to set on this
* {@code JitsiMeetView}.
*/
public void setListener(JitsiMeetViewListener listener) {
this.listener = listener;
}
/**
* Sets whether Picture-in-Picture is enabled. Because Picture-in-Picture is
* natively supported only since certain platform versions, specifying
@@ -563,7 +358,7 @@ public class JitsiMeetView extends FrameLayout {
* {@code true}; otherwise, {@code false}.
*/
public void setPictureInPictureEnabled(boolean pictureInPictureEnabled) {
this.pictureInPictureEnabled = Boolean.valueOf(pictureInPictureEnabled);
this.pictureInPictureEnabled = pictureInPictureEnabled;
}
/**

View File

@@ -22,7 +22,9 @@ import java.util.Map;
* Implements {@link JitsiMeetViewListener} so apps don't have to add stubs for
* all methods in the interface if they are only interested in some.
*/
public abstract class JitsiMeetViewAdapter implements JitsiMeetViewListener {
public abstract class JitsiMeetViewAdapter
implements JitsiMeetViewListener {
@Override
public void onConferenceFailed(Map<String, Object> data) {
}

View File

@@ -0,0 +1,166 @@
/*
* Copyright @ 2018-present Atlassian Pty 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.
*/
package org.jitsi.meet.sdk;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.ReadableMapKeySetIterator;
import com.facebook.react.bridge.UiThreadUtil;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Pattern;
/**
* Utility methods for helping with transforming {@link ExternalAPIModule}
* events into listener methods. Used with descendants of {@link BaseReactView}.
*/
public final class ListenerUtils {
/**
* Extracts the methods defined in a listener and creates a mapping of this
* form: event name -> method.
*
* @param listener - The listener whose methods we want to slurp.
* @return A mapping with event names - methods.
*/
public static Map<String, Method> mapListenerMethods(Class listener) {
Map<String, Method> methods = new HashMap<>();
// Figure out the mapping between the listener methods
// and the events i.e. redux action types.
Pattern onPattern = Pattern.compile("^on[A-Z]+");
Pattern camelcasePattern = Pattern.compile("([a-z0-9]+)([A-Z0-9]+)");
for (Method method : listener.getDeclaredMethods()) {
// * The method must be public (because it is declared by an
// interface).
// * The method must be/return void.
if (!Modifier.isPublic(method.getModifiers())
|| !Void.TYPE.equals(method.getReturnType())) {
continue;
}
// * The method name must start with "on" followed by a
// capital/uppercase letter (in agreement with the camelcase
// coding style customary to Java in general and the projects of
// the Jitsi community in particular).
String name = method.getName();
if (!onPattern.matcher(name).find()) {
continue;
}
// * The method must accept/have exactly 1 parameter of a type
// assignable from HashMap.
Class<?>[] parameterTypes = method.getParameterTypes();
if (parameterTypes.length != 1
|| !parameterTypes[0].isAssignableFrom(HashMap.class)) {
continue;
}
// Convert the method name to an event name.
name
= camelcasePattern.matcher(name.substring(2))
.replaceAll("$1_$2")
.toUpperCase(Locale.ROOT);
methods.put(name, method);
}
return methods;
}
/**
* Executes the right listener method for the given event.
* NOTE: This function will run asynchronously on the UI thread.
*
* @param listener - The listener on which the method will be called.
* @param listenerMethods - Mapping with event names and the matching
* methods.
* @param eventName - Name of the event.
* @param eventData - Data associated with the event.
*/
public static void runListenerMethod(
final Object listener,
final Map<String, Method> listenerMethods,
final String eventName,
final ReadableMap eventData) {
// Make sure listener methods are invoked on the UI thread. It
// was requested by SDK consumers.
if (UiThreadUtil.isOnUiThread()) {
runListenerMethodOnUiThread(
listener, listenerMethods, eventName, eventData);
} else {
UiThreadUtil.runOnUiThread(new Runnable() {
@Override
public void run() {
runListenerMethodOnUiThread(
listener, listenerMethods, eventName, eventData);
}
});
}
}
/**
* Helper companion for {@link ListenerUtils#runListenerMethod} which runs
* in the UI thread.
*/
private static void runListenerMethodOnUiThread(
Object listener,
Map<String, Method> listenerMethods,
String eventName,
ReadableMap eventData) {
UiThreadUtil.assertOnUiThread();
Method method = listenerMethods.get(eventName);
if (method != null) {
try {
method.invoke(listener, toHashMap(eventData));
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
} catch (InvocationTargetException e) {
throw new RuntimeException(e);
}
}
}
/**
* Initializes a new {@code HashMap} instance with the key-value
* associations of a specific {@code ReadableMap}.
*
* @param readableMap the {@code ReadableMap} specifying the key-value
* associations with which the new {@code HashMap} instance is to be
* initialized.
* @return a new {@code HashMap} instance initialized with the key-value
* associations of the specified {@code readableMap}.
*/
private static HashMap<String, Object> toHashMap(ReadableMap readableMap) {
HashMap<String, Object> hashMap = new HashMap<>();
for (ReadableMapKeySetIterator i = readableMap.keySetIterator();
i.hasNextKey();) {
String key = i.nextKey();
hashMap.put(key, readableMap.getString(key));
}
return hashMap;
}
}

View File

@@ -1,3 +1,19 @@
/*
* Copyright @ 2017-present Atlassian Pty 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.
*/
package org.jitsi.meet.sdk;
import android.app.Activity;
@@ -11,7 +27,9 @@ import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
public class PictureInPictureModule extends ReactContextBaseJavaModule {
class PictureInPictureModule
extends ReactContextBaseJavaModule {
private final static String TAG = "PictureInPicture";
static boolean isPictureInPictureSupported() {

View File

@@ -31,7 +31,9 @@ import com.facebook.react.bridge.UiThreadUtil;
* object it will dim the screen and disable touch controls. The functionality
* is used with the conference audio-only mode.
*/
class ProximityModule extends ReactContextBaseJavaModule {
class ProximityModule
extends ReactContextBaseJavaModule {
/**
* The name of {@code ProximityModule} to be used in the React Native
* bridge.

View File

@@ -0,0 +1,129 @@
/*
* Copyright @ 2018-present Atlassian Pty 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.
*/
package org.jitsi.meet.sdk;
import android.app.Activity;
import android.content.Intent;
import com.facebook.react.ReactInstanceManager;
import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler;
/**
* Helper class to encapsulate the work which needs to be done on
* {@link Activity} lifecycle methods in order for the React side to be aware of
* it.
*/
public class ReactActivityLifecycleCallbacks {
/**
* {@link Activity} lifecycle method which should be called from
* {@link Activity#onBackPressed} so we can do the required internal
* processing.
*
* @return {@code true} if the back-press was processed; {@code false},
* otherwise. If {@code false}, the application should call the
* {@code super}'s implementation.
*/
public static boolean onBackPressed() {
ReactInstanceManager reactInstanceManager
= ReactInstanceManagerHolder.getReactInstanceManager();
if (reactInstanceManager == null) {
return false;
} else {
reactInstanceManager.onBackPressed();
return true;
}
}
/**
* {@link Activity} lifecycle method which should be called from
* {@code Activity#onDestroy} so we can do the required internal
* processing.
*
* @param activity {@code Activity} being destroyed.
*/
public static void onHostDestroy(Activity activity) {
ReactInstanceManager reactInstanceManager
= ReactInstanceManagerHolder.getReactInstanceManager();
if (reactInstanceManager != null) {
reactInstanceManager.onHostDestroy(activity);
}
}
/**
* {@link Activity} lifecycle method which should be called from
* {@code Activity#onPause} so we can do the required internal processing.
*
* @param activity {@code Activity} being paused.
*/
public static void onHostPause(Activity activity) {
ReactInstanceManager reactInstanceManager
= ReactInstanceManagerHolder.getReactInstanceManager();
if (reactInstanceManager != null) {
reactInstanceManager.onHostPause(activity);
}
}
/**
* {@link Activity} lifecycle method which should be called from
* {@code Activity#onResume} so we can do the required internal processing.
*
* @param activity {@code Activity} being resumed.
*/
public static void onHostResume(Activity activity) {
onHostResume(activity, new DefaultHardwareBackBtnHandlerImpl(activity));
}
/**
* {@link Activity} lifecycle method which should be called from
* {@code Activity#onResume} so we can do the required internal processing.
*
* @param activity {@code Activity} being resumed.
* @param defaultBackButtonImpl a {@link DefaultHardwareBackBtnHandler} to
* handle invoking the back button if no {@link BaseReactView} handles it.
*/
public static void onHostResume(
Activity activity,
DefaultHardwareBackBtnHandler defaultBackButtonImpl) {
ReactInstanceManager reactInstanceManager
= ReactInstanceManagerHolder.getReactInstanceManager();
if (reactInstanceManager != null) {
reactInstanceManager.onHostResume(activity, defaultBackButtonImpl);
}
}
/**
* {@link Activity} lifecycle method which should be called from
* {@code Activity#onNewIntent} so we can do the required internal
* processing. Note that this is only needed if the activity's "launchMode"
* was set to "singleTask". This is required for deep linking to work once
* the application is already running.
*
* @param intent {@code Intent} instance which was received.
*/
public static void onNewIntent(Intent intent) {
ReactInstanceManager reactInstanceManager
= ReactInstanceManagerHolder.getReactInstanceManager();
if (reactInstanceManager != null) {
reactInstanceManager.onNewIntent(intent);
}
}
}

View File

@@ -28,7 +28,7 @@ import com.facebook.react.common.LifecycleState;
import java.util.Arrays;
import java.util.List;
public class ReactInstanceManagerHolder {
class ReactInstanceManagerHolder {
/**
* React Native bridge. The instance manager allows embedding applications
* to create multiple root views off the same JavaScript bundle.
@@ -119,14 +119,15 @@ public class ReactInstanceManagerHolder {
.setApplication(application)
.setBundleAssetName("index.android.bundle")
.setJSMainModulePath("index.android")
.addPackage(new com.BV.LinearGradient.LinearGradientPackage())
.addPackage(new com.calendarevents.CalendarEventsPackage())
.addPackage(new com.corbt.keepawake.KCKeepAwakePackage())
.addPackage(new com.dylanvann.fastimage.FastImageViewPackage())
.addPackage(new com.facebook.react.shell.MainReactPackage())
.addPackage(new com.i18n.reactnativei18n.ReactNativeI18n())
.addPackage(new com.oblador.vectoricons.VectorIconsPackage())
.addPackage(new com.ocetnik.timer.BackgroundTimerPackage())
.addPackage(new com.oney.WebRTCModule.WebRTCModulePackage())
.addPackage(new com.RNFetchBlob.RNFetchBlobPackage())
.addPackage(new com.rnimmersive.RNImmersivePackage())
.addPackage(new com.zmxv.RNSound.RNSoundPackage())
.addPackage(new ReactPackageAdapter() {

View File

@@ -25,7 +25,9 @@ import com.facebook.react.uimanager.ViewManager;
import java.util.Collections;
import java.util.List;
public class ReactPackageAdapter implements ReactPackage {
class ReactPackageAdapter
implements ReactPackage {
@Override
public List<NativeModule> createNativeModules(
ReactApplicationContext reactContext) {

View File

@@ -19,8 +19,6 @@ package org.jitsi.meet.sdk;
import android.content.Context;
import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import com.facebook.react.bridge.Promise;
@@ -36,14 +34,18 @@ import java.net.NetworkInterface;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.Enumeration;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* Module exposing WiFi statistics.
*
* Gathers rssi, signal in percentage, timestamp and the addresses
* of the wifi device.
* Gathers rssi, signal in percentage, timestamp and the addresses of the wifi
* device.
*/
class WiFiStatsModule extends ReactContextBaseJavaModule {
class WiFiStatsModule
extends ReactContextBaseJavaModule {
/**
* The name of {@code WiFiStatsModule} to be used in the React Native
* bridge.
@@ -56,17 +58,16 @@ class WiFiStatsModule extends ReactContextBaseJavaModule {
static final String TAG = MODULE_NAME;
/**
* The scale used for the signal value.
* A level of the signal, given in the range
* of 0 to SIGNAL_LEVEL_SCALE-1 (both inclusive).
* The scale used for the signal value. A level of the signal, given in the
* range of 0 to SIGNAL_LEVEL_SCALE-1 (both inclusive).
*/
public final static int SIGNAL_LEVEL_SCALE = 101;
/**
* {@link Handler} for running all operations on the main thread.
* {@link ExecutorService} for running all operations on a dedicated thread.
*/
private final Handler mainThreadHandler
= new Handler(Looper.getMainLooper());
private static final ExecutorService executor
= Executors.newSingleThreadExecutor();
/**
* Initializes a new module instance. There shall be a single instance of
@@ -119,7 +120,6 @@ class WiFiStatsModule extends ReactContextBaseJavaModule {
@Override
public void run() {
try {
Context context
= getReactApplicationContext().getApplicationContext();
WifiManager wifiManager
@@ -203,6 +203,6 @@ class WiFiStatsModule extends ReactContextBaseJavaModule {
}
}
};
mainThreadHandler.post(r);
executor.execute(r);
}
}

View File

@@ -0,0 +1,72 @@
/*
* Copyright @ 2018-present Atlassian Pty 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.
*/
package org.jitsi.meet.sdk.incoming_call;
import android.support.annotation.NonNull;
public class IncomingCallInfo {
/**
* URL for the caller avatar.
*/
private final String callerAvatarURL;
/**
* Caller's name.
*/
private final String callerName;
/**
* Whether this is a regular call or a video call.
*/
private final boolean hasVideo;
public IncomingCallInfo(
@NonNull String callerName,
@NonNull String callerAvatarURL,
boolean hasVideo) {
this.callerName = callerName;
this.callerAvatarURL = callerAvatarURL;
this.hasVideo = hasVideo;
}
/**
* Gets the caller's avatar URL.
*
* @return - The URL as a string.
*/
public String getCallerAvatarURL() {
return callerAvatarURL;
}
/**
* Gets the caller's name.
*
* @return - The caller's name.
*/
public String getCallerName() {
return callerName;
}
/**
* Gets whether the call is a video call or not.
*
* @return - {@code true} if this call has video; {@code false}, otherwise.
*/
public boolean hasVideo() {
return hasVideo;
}
}

View File

@@ -0,0 +1,73 @@
/*
* Copyright @ 2018-present Atlassian Pty 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.
*/
package org.jitsi.meet.sdk.incoming_call;
import android.content.Context;
import android.os.Bundle;
import android.support.annotation.NonNull;
import com.facebook.react.bridge.ReadableMap;
import org.jitsi.meet.sdk.BaseReactView;
import org.jitsi.meet.sdk.ListenerUtils;
import java.lang.reflect.Method;
import java.util.Map;
public class IncomingCallView
extends BaseReactView<IncomingCallViewListener> {
/**
* The {@code Method}s of {@code JitsiMeetViewListener} by event name i.e.
* redux action types.
*/
private static final Map<String, Method> LISTENER_METHODS
= ListenerUtils.mapListenerMethods(IncomingCallViewListener.class);
public IncomingCallView(@NonNull Context context) {
super(context);
}
/**
* Handler for {@link ExternalAPIModule} events.
*
* @param name The name of the event.
* @param data The details/specifics of the event to send determined
* by/associated with the specified {@code name}.
*/
@Override
public void onExternalAPIEvent(String name, ReadableMap data) {
onExternalAPIEvent(LISTENER_METHODS, name, data);
}
/**
* Sets the information for the incoming call this {@code IncomingCallView}
* represents.
*
* @param callInfo - {@link IncomingCallInfo} object representing the caller
* information.
*/
public void setIncomingCallInfo(IncomingCallInfo callInfo) {
Bundle props = new Bundle();
props.putString("callerAvatarURL", callInfo.getCallerAvatarURL());
props.putString("callerName", callInfo.getCallerName());
props.putBoolean("hasVideo", callInfo.hasVideo());
createReactRootView("IncomingCallApp", props);
}
}

View File

@@ -0,0 +1,41 @@
/*
* Copyright @ 2018-present Atlassian Pty 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.
*/
package org.jitsi.meet.sdk.incoming_call;
import java.util.Map;
/**
* Interface for listening to events coming from Jitsi Meet, related to
* {@link IncomingCallView}.
*/
public interface IncomingCallViewListener {
/**
* Called when the user presses the "Answer" button on the
* {@link IncomingCallView}.
*
* @param data - Unused at the moment.
*/
void onIncomingCallAnswered(Map<String, Object> data);
/**
* Called when the user presses the "Decline" button on the
* {@link IncomingCallView}.
*
* @param data - Unused at the moment.
*/
void onIncomingCallDeclined(Map<String, Object> data);
}

View File

@@ -33,7 +33,8 @@ import java.util.Map;
import java.util.UUID;
/**
* Controller object used by native code to query and submit user selections for the user invitation flow.
* Controller object used by native code to query and submit user selections for
* the user invitation flow.
*/
public class AddPeopleController {
@@ -44,17 +45,18 @@ public class AddPeopleController {
private AddPeopleControllerListener listener;
/**
* Local cache of search query results. Used to re-hydrate the list
* of selected items based on their ids passed to inviteById
* in order to pass the full item maps back to the JitsiMeetView during submission.
* Local cache of search query results. Used to re-hydrate the list of
* selected items based on their ids passed to inviteById in order to pass
* the full item maps back to the JitsiMeetView during submission.
*/
private final Map<String, ReadableMap> items = new HashMap<>();
private final WeakReference<InviteController> owner;
private final WeakReference<ReactApplicationContext> reactContext;
/**
* Randomly generated UUID, used for identification in the InviteModule
* Randomly generated UUID, used for identification in the InviteModule.
*/
private final String uuid = UUID.randomUUID().toString();
@@ -158,10 +160,10 @@ public class AddPeopleController {
}
/**
* Caches results received by the search into a local map for use
* later when the items are submitted. Submission requires the full
* map of information, but only the IDs are returned back to the delegate.
* Using this map means we don't have to send the whole map back to the delegate.
* Caches results received by the search into a local map for use later when
* the items are submitted. Submission requires the full map of
* information, but only the IDs are returned back to the delegate. Using
* this map means we don't have to send the whole map back to the delegate.
*
* @param results
* @param query
@@ -179,10 +181,15 @@ public class AddPeopleController {
if(map.hasKey("id")) {
items.put(map.getString("id"), map);
} else if(map.hasKey("type") && map.getString("type").equals("phone") && map.hasKey("number")) {
} else if(map.hasKey("type")
&& map.getString("type").equals("phone")
&& map.hasKey("number")) {
items.put(map.getString("number"), map);
} else {
Log.w("AddPeopleController", "Received result without id and that was not a phone number, so not adding it to suggestions: " + map);
Log.w(
"AddPeopleController",
"Received result without id and that was not a phone number, so not adding it to suggestions: "
+ map);
}
jvmResults.add(map.toHashMap());

View File

@@ -202,9 +202,9 @@ public class InviteController {
}
/**
* Starts a query for users to invite to the conference. Results will be
* returned through the {@link AddPeopleControllerListener#onReceivedResults(AddPeopleController, List, String)}
* method.
* Starts a query for users to invite to the conference. Results will be
* returned through
* {@link AddPeopleControllerListener#onReceivedResults(AddPeopleController, List, String)}.
*
* @param query {@code String} to use for the query
*/

View File

@@ -20,10 +20,10 @@ public interface InviteControllerListener {
/**
* Called when the add user button is tapped.
*
* @param addPeopleController {@code AddPeopleController} scoped
* for this user invite flow. The {@code AddPeopleController} is used
* to start user queries and accepts an {@code AddPeopleControllerListener}
* for receiving user query responses.
* @param addPeopleController {@code AddPeopleController} scoped for this
* user invite flow. The {@code AddPeopleController} is used to start user
* queries and accepts an {@code AddPeopleControllerListener} for receiving
* user query responses.
*/
void beginAddPeople(AddPeopleController addPeopleController);
}

View File

@@ -24,12 +24,15 @@ import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.UiThreadUtil;
import org.jitsi.meet.sdk.BaseReactView;
import org.jitsi.meet.sdk.JitsiMeetView;
/**
* Implements the react-native module of the feature invite.
*/
public class InviteModule extends ReactContextBaseJavaModule {
public class InviteModule
extends ReactContextBaseJavaModule {
public InviteModule(ReactApplicationContext reactContext) {
super(reactContext);
}
@@ -67,7 +70,8 @@ public class InviteModule extends ReactContextBaseJavaModule {
private InviteController findInviteControllerByExternalAPIScope(
String externalAPIScope) {
JitsiMeetView view
= JitsiMeetView.findViewByExternalAPIScope(externalAPIScope);
= (JitsiMeetView)
BaseReactView.findViewByExternalAPIScope(externalAPIScope);
return view == null ? null : view.getInviteController();
}
@@ -81,7 +85,8 @@ public class InviteModule extends ReactContextBaseJavaModule {
* Callback for invitation failures
*
* @param failedInvitees the items for which the invitation failed
* @param addPeopleControllerScope a string that represents a connection to a specific AddPeopleController
* @param addPeopleControllerScope a string that represents a connection to
* a specific AddPeopleController
*/
@ReactMethod
public void inviteSettled(
@@ -123,7 +128,8 @@ public class InviteModule extends ReactContextBaseJavaModule {
*
* @param results the results in a ReadableArray of ReadableMap objects
* @param query the query associated with the search
* @param addPeopleControllerScope a string that represents a connection to a specific AddPeopleController
* @param addPeopleControllerScope a string that represents a connection to
* a specific AddPeopleController
*/
@ReactMethod
public void receivedResults(

View File

@@ -32,7 +32,9 @@ import java.net.UnknownHostException;
* [1]: https://tools.ietf.org/html/rfc6146
* [2]: https://tools.ietf.org/html/rfc6052
*/
public class NAT64AddrInfoModule extends ReactContextBaseJavaModule {
public class NAT64AddrInfoModule
extends ReactContextBaseJavaModule {
/**
* The host for which the module wil try to resolve both IPv4 and IPv6
* addresses in order to figure out the NAT64 prefix.

View File

@@ -3,12 +3,14 @@ rootProject.name = 'jitsi-meet'
include ':app', ':sdk'
include ':react-native-background-timer'
project(':react-native-background-timer').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-background-timer/android')
include ':react-native-fetch-blob'
project(':react-native-fetch-blob').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-fetch-blob/android')
include ':react-native-fast-image'
project(':react-native-fast-image').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-fast-image/android')
include ':react-native-immersive'
project(':react-native-immersive').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-immersive/android')
include ':react-native-keep-awake'
project(':react-native-keep-awake').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-keep-awake/android')
include ':react-native-linear-gradient'
project(':react-native-linear-gradient').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-linear-gradient/android')
include ':react-native-locale-detector'
project(':react-native-locale-detector').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-locale-detector/android')
include ':react-native-sound'

View File

@@ -7,8 +7,6 @@ import Recorder from './modules/recorder/Recorder';
import mediaDeviceHelper from './modules/devices/mediaDeviceHelper';
import { reportError } from './modules/util/helpers';
import * as RemoteControlEvents
from './service/remotecontrol/RemoteControlEvents';
import UIEvents from './service/UI/UIEvents';
@@ -18,7 +16,6 @@ import * as JitsiMeetConferenceEvents from './ConferenceEvents';
import {
createDeviceChangedEvent,
createScreenSharingEvent,
createSelectParticipantFailedEvent,
createStreamSwitchDelayEvent,
createTrackMutedEvent,
sendAnalytics
@@ -33,10 +30,12 @@ import EventEmitter from 'events';
import {
AVATAR_ID_COMMAND,
AVATAR_URL_COMMAND,
authStatusChanged,
conferenceFailed,
conferenceJoined,
conferenceLeft,
conferenceWillJoin,
conferenceWillLeave,
dataChannelOpened,
EMAIL_COMMAND,
lockStateChanged,
@@ -46,6 +45,7 @@ import {
setDesktopSharingEnabled
} from './react/features/base/conference';
import {
getAvailableDevices,
setAudioOutputDeviceId,
updateDeviceList
} from './react/features/base/devices';
@@ -74,6 +74,8 @@ import {
getAvatarURLByParticipantId,
getLocalParticipant,
getParticipantById,
hiddenParticipantJoined,
hiddenParticipantLeft,
localParticipantConnectionStatusChanged,
localParticipantRoleChanged,
MAX_DISPLAY_NAME_LENGTH,
@@ -108,6 +110,7 @@ import {
} from './react/features/overlay';
import { setSharedVideoStatus } from './react/features/shared-video';
import { isButtonEnabled } from './react/features/toolbox';
import { endpointMessageReceived } from './react/features/subtitles';
const logger = require('jitsi-meet-logger').getLogger(__filename);
@@ -371,6 +374,8 @@ class ConferenceConnector {
case JitsiConferenceErrors.FOCUS_LEFT:
case JitsiConferenceErrors.VIDEOBRIDGE_NOT_AVAILABLE:
APP.store.dispatch(conferenceWillLeave(room));
// FIXME the conference should be stopped by the library and not by
// the app. Both the errors above are unrecoverable from the library
// perspective.
@@ -467,6 +472,7 @@ function _connectionFailedHandler(error) {
JitsiConnectionEvents.CONNECTION_FAILED,
_connectionFailedHandler);
if (room) {
APP.store.dispatch(conferenceWillLeave(room));
room.leave();
}
}
@@ -1650,17 +1656,21 @@ export default {
room.on(
JitsiConferenceEvents.AUTH_STATUS_CHANGED,
(authEnabled, authLogin) =>
APP.UI.updateAuthInfo(authEnabled, authLogin));
APP.store.dispatch(authStatusChanged(authEnabled, authLogin)));
room.on(JitsiConferenceEvents.PARTCIPANT_FEATURES_CHANGED,
user => APP.UI.onUserFeaturesChanged(user));
room.on(JitsiConferenceEvents.USER_JOINED, (id, user) => {
if (user.isHidden()) {
return;
}
const displayName = user.getDisplayName();
if (user.isHidden()) {
APP.store.dispatch(hiddenParticipantJoined(id, displayName));
return;
}
APP.store.dispatch(participantJoined({
botType: user.getBotType(),
conference: room,
id,
name: displayName,
@@ -1682,8 +1692,11 @@ export default {
room.on(JitsiConferenceEvents.USER_LEFT, (id, user) => {
if (user.isHidden()) {
APP.store.dispatch(hiddenParticipantLeft(id));
return;
}
APP.store.dispatch(participantLeft(id, room));
logger.log('USER %s LEFT', id, user);
APP.API.notifyUserLeft(id);
@@ -1784,13 +1797,6 @@ export default {
id => APP.store.dispatch(dominantSpeakerChanged(id, room)));
if (!interfaceConfig.filmStripOnly) {
room.on(JitsiConferenceEvents.CONNECTION_INTERRUPTED, () => {
APP.UI.markVideoInterrupted(true);
});
room.on(JitsiConferenceEvents.CONNECTION_RESTORED, () => {
APP.UI.markVideoInterrupted(false);
});
if (isButtonEnabled('chat')) {
room.on(
JitsiConferenceEvents.MESSAGE_RECEIVED,
@@ -1817,38 +1823,16 @@ export default {
room.sendTextMessage(message);
});
}
APP.UI.addListener(UIEvents.SELECTED_ENDPOINT, id => {
APP.API.notifyOnStageParticipantChanged(id);
try {
// do not try to select participant if there is none (we
// are alone in the room), otherwise an error will be
// thrown cause reporting mechanism is not available
// (datachannels currently)
if (room.getParticipants().length === 0) {
return;
}
room.selectParticipant(id);
} catch (e) {
sendAnalytics(createSelectParticipantFailedEvent(e));
reportError(e);
}
});
}
room.on(JitsiConferenceEvents.CONNECTION_INTERRUPTED, () => {
APP.store.dispatch(localParticipantConnectionStatusChanged(
JitsiParticipantConnectionStatus.INTERRUPTED));
APP.UI.showLocalConnectionInterrupted(true);
});
room.on(JitsiConferenceEvents.CONNECTION_RESTORED, () => {
APP.store.dispatch(localParticipantConnectionStatusChanged(
JitsiParticipantConnectionStatus.ACTIVE));
APP.UI.showLocalConnectionInterrupted(false);
});
room.on(
@@ -1872,6 +1856,21 @@ export default {
APP.UI.changeDisplayName(id, formattedDisplayName);
}
);
room.on(
JitsiConferenceEvents.BOT_TYPE_CHANGED,
(id, botType) => {
APP.store.dispatch(participantUpdated({
conference: room,
id,
botType
}));
}
);
room.on(
JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED,
(...args) => APP.store.dispatch(endpointMessageReceived(...args)));
room.on(
JitsiConferenceEvents.LOCK_STATE_CHANGED,
@@ -1916,31 +1915,6 @@ export default {
}
});
/* eslint-enable max-params */
room.on(
JitsiConferenceEvents.RECORDER_STATE_CHANGED,
recorderSession => {
if (!recorderSession) {
logger.error(
'Received invalid recorder status update',
recorderSession);
return;
}
// These errors fire when the local participant has requested a
// recording but the request itself failed, hence the missing
// session ID because the recorder never started.
if (recorderSession.getError()) {
this._showRecordingErrorNotification(recorderSession);
return;
}
logger.error(
'Received a recorder status update with no ID or error');
});
room.on(JitsiConferenceEvents.KICKED, () => {
APP.UI.hideStats();
APP.UI.notifyKicked();
@@ -1990,14 +1964,14 @@ export default {
}
);
APP.UI.addListener(UIEvents.EMAIL_CHANGED, this.changeLocalEmail);
APP.UI.addListener(UIEvents.EMAIL_CHANGED,
this.changeLocalEmail.bind(this));
room.addCommandListener(this.commands.defaults.EMAIL, (data, from) => {
APP.store.dispatch(participantUpdated({
conference: room,
id: from,
email: data.value
}));
APP.UI.setUserEmail(from, data.value);
});
room.addCommandListener(
@@ -2156,20 +2130,6 @@ export default {
}
);
APP.UI.addListener(
UIEvents.AUDIO_OUTPUT_DEVICE_CHANGED,
audioOutputDeviceId => {
sendAnalytics(createDeviceChangedEvent('audio', 'output'));
setAudioOutputDeviceId(audioOutputDeviceId)
.then(() => logger.log('changed audio output device'))
.catch(err => {
logger.warn('Failed to change audio output device. '
+ 'Default or previously set audio output device '
+ 'will be used instead.', err);
});
}
);
APP.UI.addListener(UIEvents.TOGGLE_AUDIO_ONLY, audioOnly => {
// FIXME On web video track is stored both in redux and in
@@ -2301,10 +2261,10 @@ export default {
APP.store.dispatch(conferenceJoined(room));
APP.UI.mucJoined();
const displayName
= APP.store.getState()['features/base/settings'].displayName;
APP.UI.changeDisplayName('localVideoContainer', displayName);
APP.API.notifyConferenceJoined(
this.roomName,
this._room.myUserId(),
@@ -2317,7 +2277,6 @@ export default {
APP.store.getState(), this._room.myUserId())
}
);
APP.UI.markVideoInterrupted(false);
},
/**
@@ -2342,47 +2301,43 @@ export default {
/**
* Inits list of current devices and event listener for device change.
* @private
* @returns {Promise}
*/
_initDeviceList() {
JitsiMeetJS.mediaDevices.isDeviceListAvailable()
.then(isDeviceListAvailable => {
if (isDeviceListAvailable
&& JitsiMeetJS.mediaDevices.isDeviceChangeAvailable()) {
JitsiMeetJS.mediaDevices.enumerateDevices(devices => {
// Ugly way to synchronize real device IDs with local
// storage and settings menu. This is a workaround until
// getConstraints() method will be implemented
// in browsers.
const { dispatch } = APP.store;
const { mediaDevices } = JitsiMeetJS;
if (this.localAudio) {
dispatch(updateSettings({
micDeviceId: this.localAudio.getDeviceId()
}));
}
if (mediaDevices.isDeviceListAvailable()
&& mediaDevices.isDeviceChangeAvailable()) {
this.deviceChangeListener = devices =>
window.setTimeout(() => this._onDeviceListChanged(devices), 0);
mediaDevices.addEventListener(
JitsiMediaDevicesEvents.DEVICE_LIST_CHANGED,
this.deviceChangeListener);
if (this.localVideo) {
dispatch(updateSettings({
cameraDeviceId: this.localVideo.getDeviceId()
}));
}
const { dispatch } = APP.store;
mediaDeviceHelper.setCurrentMediaDevices(devices);
APP.UI.onAvailableDevicesChanged(devices);
APP.store.dispatch(updateDeviceList(devices));
});
return dispatch(getAvailableDevices())
.then(devices => {
// Ugly way to synchronize real device IDs with local
// storage and settings menu. This is a workaround until
// getConstraints() method will be implemented in browsers.
if (this.localAudio) {
dispatch(updateSettings({
micDeviceId: this.localAudio.getDeviceId()
}));
}
this.deviceChangeListener = devices =>
window.setTimeout(
() => this._onDeviceListChanged(devices), 0);
JitsiMeetJS.mediaDevices.addEventListener(
JitsiMediaDevicesEvents.DEVICE_LIST_CHANGED,
this.deviceChangeListener);
}
})
.catch(error => {
logger.warn(`Error getting device list: ${error}`);
});
if (this.localVideo) {
dispatch(updateSettings({
cameraDeviceId: this.localVideo.getDeviceId()
}));
}
APP.UI.onAvailableDevicesChanged(devices);
});
}
return Promise.resolve();
},
/**
@@ -2393,16 +2348,7 @@ export default {
* @returns {Promise}
*/
_onDeviceListChanged(devices) {
let currentDevices = mediaDeviceHelper.getCurrentMediaDevices();
// Event handler can be fired before direct
// enumerateDevices() call, so handle this situation here.
if (!currentDevices.audioinput
&& !currentDevices.videoinput
&& !currentDevices.audiooutput) {
mediaDeviceHelper.setCurrentMediaDevices(devices);
currentDevices = mediaDeviceHelper.getCurrentMediaDevices();
}
APP.store.dispatch(updateDeviceList(devices));
const newDevices
= mediaDeviceHelper.getNewMediaDevicesAfterDeviceListChanged(
@@ -2415,9 +2361,13 @@ export default {
const videoWasMuted = this.isLocalVideoMuted();
if (typeof newDevices.audiooutput !== 'undefined') {
// Just ignore any errors in catch block.
promises.push(setAudioOutputDeviceId(newDevices.audiooutput)
.catch());
const { dispatch } = APP.store;
const setAudioOutputPromise
= setAudioOutputDeviceId(newDevices.audiooutput, dispatch)
.catch(); // Just ignore any errors in catch block.
promises.push(setAudioOutputPromise);
}
promises.push(
@@ -2451,7 +2401,6 @@ export default {
return Promise.all(promises)
.then(() => {
mediaDeviceHelper.setCurrentMediaDevices(devices);
APP.UI.onAvailableDevicesChanged(devices);
});
},
@@ -2461,7 +2410,7 @@ export default {
*/
updateAudioIconEnabled() {
const audioMediaDevices
= mediaDeviceHelper.getCurrentMediaDevices().audioinput;
= APP.store.getState()['features/base/devices'].audioInput;
const audioDeviceCount
= audioMediaDevices ? audioMediaDevices.length : 0;
@@ -2484,7 +2433,7 @@ export default {
*/
updateVideoIconEnabled() {
const videoMediaDevices
= mediaDeviceHelper.getCurrentMediaDevices().videoinput;
= APP.store.getState()['features/base/devices'].videoInput;
const videoDeviceCount
= videoMediaDevices ? videoMediaDevices.length : 0;
@@ -2515,9 +2464,11 @@ export default {
APP.UI.removeLocalMedia();
// Remove unnecessary event listeners from firing callbacks.
JitsiMeetJS.mediaDevices.removeEventListener(
JitsiMediaDevicesEvents.DEVICE_LIST_CHANGED,
this.deviceChangeListener);
if (this.deviceChangeListener) {
JitsiMeetJS.mediaDevices.removeEventListener(
JitsiMediaDevicesEvents.DEVICE_LIST_CHANGED,
this.deviceChangeListener);
}
let requestFeedbackPromise;
@@ -2537,13 +2488,24 @@ export default {
// before all operations are done.
Promise.all([
requestFeedbackPromise,
room.leave().then(disconnect, disconnect)
this.leaveRoomAndDisconnect()
]).then(values => {
APP.API.notifyReadyToClose();
maybeRedirectToWelcomePage(values[0]);
});
},
/**
* Leaves the room and calls JitsiConnection.disconnect.
*
* @returns {Promise}
*/
leaveRoomAndDisconnect() {
APP.store.dispatch(conferenceWillLeave(room));
return room.leave().then(disconnect, disconnect);
},
/**
* Changes the email for the local user
* @param email {string} the new email
@@ -2574,8 +2536,9 @@ export default {
APP.store.dispatch(updateSettings({
email: formattedEmail
}));
APP.UI.setUserEmail(localId, formattedEmail);
APP.API.notifyEmailChanged(localId, {
email: formattedEmail
});
sendData(commands.EMAIL, formattedEmail);
},
@@ -2741,57 +2704,5 @@ export default {
if (score === -1 || (score >= 1 && score <= 5)) {
APP.store.dispatch(submitFeedback(score, message, room));
}
},
/**
* Shows a notification about an error in the recording session. A
* default notification will display if no error is specified in the passed
* in recording session.
*
* @param {Object} recorderSession - The recorder session model from the
* lib.
* @private
* @returns {void}
*/
_showRecordingErrorNotification(recorderSession) {
const isStreamMode
= recorderSession.getMode()
=== JitsiMeetJS.constants.recording.mode.STREAM;
switch (recorderSession.getError()) {
case JitsiMeetJS.constants.recording.error.SERVICE_UNAVAILABLE:
APP.UI.messageHandler.showError({
descriptionKey: 'recording.unavailable',
descriptionArguments: {
serviceName: isStreamMode
? 'Live Streaming service'
: 'Recording service'
},
titleKey: isStreamMode
? 'liveStreaming.unavailableTitle'
: 'recording.unavailableTitle'
});
break;
case JitsiMeetJS.constants.recording.error.RESOURCE_CONSTRAINT:
APP.UI.messageHandler.showError({
descriptionKey: isStreamMode
? 'liveStreaming.busy'
: 'recording.busy',
titleKey: isStreamMode
? 'liveStreaming.busyTitle'
: 'recording.busyTitle'
});
break;
default:
APP.UI.messageHandler.showError({
descriptionKey: isStreamMode
? 'liveStreaming.error'
: 'recording.error',
titleKey: isStreamMode
? 'liveStreaming.failedToStart'
: 'recording.failedToStart'
});
break;
}
}
};

View File

@@ -99,13 +99,13 @@ var config = {
// used by browsers that return true from lib-jitsi-meet's
// util#browser#usesNewGumFlow. The constraints are independency from
// this config's resolution value. Defaults to requesting an ideal aspect
// ratio of 16:9 with an ideal resolution of 1080p.
// ratio of 16:9 with an ideal resolution of 720.
// constraints: {
// video: {
// aspectRatio: 16 / 9,
// height: {
// ideal: 1080,
// max: 1080,
// ideal: 720,
// max: 720,
// min: 240
// }
// }
@@ -175,6 +175,10 @@ var config = {
// Whether to enable live streaming or not.
// liveStreamingEnabled: false,
// Transcription (in interface_config,
// subtitles and buttons can be configured)
// transcribingEnabled: false,
// Misc
// Default value for the channel "last N" attribute. -1 for unlimited.
@@ -252,7 +256,6 @@ var config = {
// maintenance at 01:00 AM GMT,
// noticeMessage: '',
// Stats
//
@@ -346,6 +349,7 @@ var config = {
// List of undocumented settings used in jitsi-meet
/**
_immediateReloadThreshold
autoRecord
autoRecordToken
debug

View File

@@ -132,7 +132,7 @@ function connect(id, password, roomName) {
*
*/
function handleConnectionEstablished() {
APP.store.dispatch(connectionEstablished(connection));
APP.store.dispatch(connectionEstablished(connection, Date.now()));
unsubscribe();
resolve(connection);
}

View File

@@ -0,0 +1,12 @@
/**
* Move Atlaskit Flag up a little bit so it does not cover the toolbar with the
* first notification.
*/
.cxGWJB{
bottom: calc(#{$newToolbarSizeWithPadding}) !important;
}
.gXSEsl:nth-child(n+2) {
transform: translateX(0) translateY(100%) translateY(16px) !important;
-ms-transform: translateX(0) translateY(100%) translateY(16px) !important;
-webkit-transform: translateX(0) translateY(100%) translateY(16px) !important;
}

View File

@@ -14,6 +14,11 @@ textarea {
user-select: text;
}
html {
height: 100%;
width: 100%;
}
body {
margin: 0px;
width: 100%;
@@ -103,14 +108,15 @@ form {
}
.leftwatermark {
left: $defaultToolbarSize;
margin-left: 10px;
left: 32px;
top: 32px;
background-image: url($defaultWatermarkLink);
background-position: center left;
}
.rightwatermark {
right: 15;
right: 32px;
top: 32px;
background-position: center right;
}

View File

@@ -6,7 +6,7 @@
#chatconversation {
visibility: hidden;
position: relative;
top: 5px;
top: 15px;
padding: 5px;
text-align: left;
line-height: 20px;
@@ -60,37 +60,7 @@
}
.localuser {
color: #087dba;
}
.use-new-toolbox {
.chatmessage {
color: white;
}
.localuser {
color: #4C9AFF;
}
.remoteuser {
color: #B8C7E0;
}
#usermsg {
color: white;
}
.chatmessage,
#smileysarea,
#smileysContainer,
#usermsg {
background-color: $newToolbarBackgroundColor;
}
.smileyContainer:hover {
background-color: $newToolbarButtonToggleColor;
border-radius: 5px;
cursor: pointer;
}
color: #4C9AFF
}
.errorMessage {
@@ -98,10 +68,11 @@
}
.remoteuser {
color: white;
color: #B8C7E0;
}
#usermsg {
background-color: $newToolbarBackgroundColor;
visibility:hidden;
position: absolute;
bottom: 0px;
@@ -112,8 +83,7 @@
max-height:150px;
min-height:35px;
border: 0px none;
background: #3a3a3a;
color: #a7a7a7;
color: white;
box-shadow: none;
border-radius:0;
font-size: 10pt;
@@ -183,7 +153,7 @@
}
.chatmessage {
background: #3a3a3a;
background-color: $newToolbarBackgroundColor;;
width: 93%;
margin-left: 9px;
margin-right: auto;
@@ -191,7 +161,7 @@
border-top-left-radius: 0px;
margin-top: 3px;
left: 5px;
color: #a7a7a7;
color: white;
overflow: hidden;
padding-bottom: 3px;
}
@@ -227,7 +197,7 @@
max-height:150px;
min-height:35px;
border: 0px none;
background: #3a3a3a;
background-color: $newToolbarBackgroundColor;
overflow: hidden;
visibility: hidden;
}
@@ -239,7 +209,7 @@
#smileysContainer {
display: none;
position: absolute;
background: #3a3a3a;
background-color: $newToolbarBackgroundColor;
border-bottom: 1px solid;
border-top: 1px solid;
width: 100%;
@@ -257,7 +227,9 @@
}
.smileyContainer:hover {
background: #3e3e3e;
background-color: $newToolbarButtonToggleColor;
border-radius: 5px;
cursor: pointer;
}
#usermsg::-webkit-input-placeholder {

View File

@@ -1,191 +0,0 @@
%align-right {
@include flex();
flex-direction: row-reverse;
flex-wrap: nowrap;
justify-content: flex-start;
}
.use-new-toolbox {
.filmstrip.reduce-height {
bottom: $newToolbarSizeWithPadding;
}
.filmstrip {
transition: bottom .3s;
}
.filmstrip__videos.hidden {
bottom: calc(-196px - #{$newToolbarSizeWithPadding});
}
}
.filmstrip {
position: absolute;
bottom: 0;
right: 0;
padding: 10px 5px;
@extend %align-right;
z-index: $filmstripVideosZ;
&__toolbar {
@include flex();
flex-direction: column-reverse;
flex-wrap: nowrap;
position: relative;
width: $filmstripToggleButtonWidth;
button {
font-size: 14px;
line-height: 1.2;
text-align: center;
background: transparent;
opacity: 0.7;
height: auto;
width: 100%;
padding: 0;
margin: 0;
border: none;
outline: none;
-webkit-appearance: none;
&:hover {
opacity: 1;
}
i {
cursor: pointer;
}
}
}
&__videos {
@extend %align-right;
position:relative;
padding: 0;
/* The filmstrip should not be covered by the left toolbar. */
bottom: 0;
width:auto;
overflow: visible !important;
&#remoteVideos {
border: $thumbnailsBorder solid transparent;
padding-left: $defaultToolbarSize + 5;
transition: bottom 2s;
}
/**
* The local video identifier.
*/
&#filmstripLocalVideo {
align-self: flex-end;
display: block;
}
&.hidden {
bottom: -196px;
}
.remote-videos-container {
display: flex;
}
.videocontainer {
display: none;
position: relative;
background-size: contain;
border: $thumbnailVideoBorder solid transparent;
border-radius: $borderRadius;
margin: 0 $thumbnailVideoMargin;
&.videoContainerFocused, &:hover {
cursor: hand;
}
/**
* Focused video thumbnail.
*/
&.videoContainerFocused {
transition-duration: 0.5s;
-webkit-transition-duration: 0.5s;
-webkit-animation-name: greyPulse;
-webkit-animation-duration: 2s;
-webkit-animation-iteration-count: 1;
border: $thumbnailVideoBorder solid $videoThumbnailSelected !important;
box-shadow: inset 0 0 3px $videoThumbnailSelected,
0 0 3px $videoThumbnailSelected !important;
}
.remotevideomenu > .icon-menu {
display: none;
}
.presence-label {
color: $participantNameColor;
font-size: 12px;
font-weight: 100;
left: 0;
margin: 0 auto;
overflow: hidden;
pointer-events: none;
position: absolute;
right: 0;
text-align: center;
text-overflow: ellipsis;
top: calc(50% + 30px);
white-space: nowrap;
width: 100%;
z-index: $zindex3;
}
/**
* Hovered video thumbnail.
*/
&:hover {
cursor: hand;
border: $thumbnailVideoBorder solid $videoThumbnailHovered;
box-shadow: inset 0 0 3px $videoThumbnailHovered,
0 0 3px $videoThumbnailHovered;
.remotevideomenu > .icon-menu {
display: inline-block;
}
}
/* With the TemasysWebRTC plugin <object/> element is used
instead of <video/> */
& > video,
& > object {
cursor: hand;
border-radius: $borderRadius;
object-fit: cover;
overflow: hidden;
}
}
}
/**
* Style the filmstrip videos in filmstrip-only mode.
*/
&__videos-filmstripOnly {
margin-top: auto;
margin-bottom: auto;
.filmstrip__videos {
&#filmstripLocalVideo {
bottom: 0px;
}
}
}
.remote-videos-container {
transition: opacity 1s;
}
&.hide-videos {
.remote-videos-container {
opacity: 0;
pointer-events: none;
}
}
}

View File

@@ -24,9 +24,13 @@
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.icon-arrow_back:before {
content: "\e5c4";
}
.icon-close:before {
content: "\e5cd";
}
.icon-event_note:before {
content: "\e616";
}
@@ -204,3 +208,15 @@
.icon-live:before {
content: "\e92c";
}
.icon-speaker:before {
content: "\e92d";
}
.icon-tiles-many:before {
content: "\e92e";
}
.icon-tiles-one:before {
content: "\e92f";
}
.icon-closed_caption:before {
content: "\e930";
}

View File

@@ -0,0 +1,43 @@
%navigate-section-list-text {
width: 100%;
font-size: 14px;
line-height: 20px;
color: $welcomePageTitleColor;
text-align: left;
font-family: 'open_sanslight', Helvetica, sans-serif;
}
%navigate-section-list-tile-text {
@extend %navigate-section-list-text;
overflow: hidden;
text-overflow: ellipsis;
float: left;
}
.navigate-section-list-tile {
height: 90px;
width: 260px;
border-radius: 4px;
background-color: #1754A9;
margin-right: 8px;
padding: 16px;
display: inline-block;
box-sizing: border-box;
cursor: pointer;
}
.navigate-section-tile-body {
@extend %navigate-section-list-tile-text;
font-weight: normal;
}
.navigate-section-tile-title {
@extend %navigate-section-list-tile-text;
font-weight: bold;
}
.navigate-section-section-header {
@extend %navigate-section-list-text;
font-weight: bold;
margin-bottom: 16px;
}
.navigate-section-list {
position: relative;
margin-top: 36px;
margin-bottom: 36px;
}

View File

@@ -10,14 +10,24 @@
right: 0;
width: 100%;
}
.popover-mousemove-padding-right {
%vertical-popover-padding {
height: 100%;
position: absolute;
right: -20;
top: 0;
width: 40px;
}
.popover-mousemove-padding-left {
@extend %vertical-popover-padding;
left: -20px;
}
.popover-mousemove-padding-right {
@extend %vertical-popover-padding;
right: -20px;
}
/**
* An invisible element is added to the top of the popover to ensure the mouse
* stays over the popover when the popover's height is shrunk, which would then

View File

@@ -30,6 +30,10 @@
width: 100%;
}
.google-error {
color: $errorColor;
}
/**
* The Google sign in button must follow Google's design guidelines.
* See: https://developers.google.com/identity/branding-guidelines

View File

@@ -1,41 +1,14 @@
/**
* Toolbar side panel main container element.
*/
.use-new-toolbox #sideToolbarContainer {
#sideToolbarContainer {
background-color: $newToolbarBackgroundColor;
/**
* Make the sidebar flush with the top of the toolbar. Take the size of
* the toolbar and subtract from 100%.
*/
height: calc(100% - #{$newToolbarSizeWithPadding});
left: 0;
.side-toolbar-close {
background: gray;
border: 3px solid rgba(255, 255, 255, 0.1);
border-radius: 100%;
color: white;
cursor:pointer;
height: 10px;
line-height: 10px;
padding: 4px;
position: absolute;
right: 5px;
text-align: center;
top: 5px;
width: 10px;
z-index: 1;
}
#chatconversation {
top: 15px;
}
}
#sideToolbarContainer {
background-color: $sideToolbarContainerBg;
height: 100%;
left: $defaultToolbarSize;
max-width: $sidebarWidth;
overflow: hidden;
position: absolute;
@@ -92,7 +65,7 @@
/**
* Titles and subtitles of inner containers.
*/
div.title, div.subTitle {
div.title {
margin: 24px 0 11px;
}
@@ -113,52 +86,20 @@
}
}
.settings-menu {
display: flex;
flex-direction: column;
padding-left: 10%;
padding-right: 10%;
.moderator-checkbox {
display: inline-block;
margin: 0 5px 0;
width: auto;
}
.moderator-option {
margin-top: 15px;
}
.subTitle {
color: $defaultSideBarFontColor;
font-size: 11px;
font-weight: 500;
}
}
}
/**
* Profile
*/
.auth_container {
ul {
padding: 0;
li {
list-style-type: none;
a.authButton {
width: 160px;
margin: 10px 20px;
padding: 3px 29px;
box-sizing: border-box;
background-color: #06a5df;
border-radius: 4px;
cursor: pointer;
color: $defaultColor;
text-decoration: none;
text-align: center;
}
}
.side-toolbar-close {
background: gray;
border: 3px solid rgba(255, 255, 255, 0.1);
border-radius: 100%;
color: white;
cursor:pointer;
height: 10px;
line-height: 10px;
padding: 4px;
position: absolute;
right: 5px;
text-align: center;
top: 5px;
width: 10px;
z-index: 1;
}
}

View File

@@ -19,17 +19,6 @@
vertical-align: middle;
}
.use-new-toolbox {
.cxGWJB{
bottom: calc(#{$newToolbarSizeWithPadding});
}
.gXSEsl:nth-child(n+2) {
transform: translateX(0) translateY(100%) translateY(16px);
-ms-transform: translateX(0) translateY(100%) translateY(16px);
-webkit-transform: translateX(0) translateY(100%) translateY(16px);
}
}
/**
* TODO: when the old filmstrip has been removed, remove the "new-" prefix.
*/
@@ -131,6 +120,12 @@
height: 22px;
padding: 5px 12px;
div {
display: flex;
flex-direction: row;
align-items: center;
}
&:hover {
background: #313D52;
}
@@ -151,7 +146,6 @@
background: #B8C7E0;
border-radius: 2px;
color: $newToolbarBackgroundColor;
font-family: -apple-system, BlinkMacSystemFont, $baseFontFamily;
font-size: 11px;
font-weight: bold;
margin-left: 8px;

View File

@@ -0,0 +1,13 @@
.transcription-subtitles{
bottom: 10%;
font-size: 16px;
font-weight: 1000;
opacity: 0.80;
position: absolute;
text-shadow: 0px 0px 1px rgba(0,0,0,0.3),
0px 1px 1px rgba(0,0,0,0.3),
1px 0px 1px rgba(0,0,0,0.3),
0px 0px 1px rgba(0,0,0,0.3);
width: 100%;
z-index: $zindex2;
}

View File

@@ -3,7 +3,7 @@
/**
* Style variables
*/
$baseFontFamily: 'open_sanslight', 'Helvetica Neue', Helvetica, Arial, sans-serif;
$baseFontFamily: -apple-system, BlinkMacSystemFont, 'open_sanslight', 'Helvetica Neue', Helvetica, Arial, sans-serif;
$hangupColor: #bf2117;
$hangupFontSize: 2em;
@@ -144,7 +144,7 @@ $watermarkHeight: 74px;
/**
* Welcome page variables.
*/
$welcomePageDescriptionColor: #fff;
$welcomePageDescriptionColor: #E6EDFA;
$welcomePageFontFamily: inherit;
$welcomePageHeaderBackground: linear-gradient(#165ecc, #44A5FF);
$welcomePageHeaderBackground: #1D69D4;
$welcomePageTitleColor: #fff;

View File

@@ -1,264 +0,0 @@
/**
* Override other styles to support vertical filmstrip mode.
*/
.vertical-filmstrip {
/*
* Firefox sets flex items to min-height: auto and min-width: auto,
* preventing flex children from shrinking like they do on other browsers.
* Setting min-height and min-width 0 is a workaround for the issue so
* Firefox behaves like other browsers.
* https://bugzilla.mozilla.org/show_bug.cgi?id=1043520
*/
@mixin minHWAutoFix() {
min-height: 0;
min-width: 0;
}
#etherpad,
#sharedvideo {
text-align: left;
}
&.use-new-toolbox {
/**
* Adjust the height of the filmstrip as the toolbar is displayed.
*/
.filmstrip {
top: 0;
transition: height .3s ease-in;
&.reduce-height {
height: calc(100% - #{$newToolbarSizeWithPadding});
}
}
}
.filmstrip {
align-items: flex-end;
box-sizing: border-box;
display: flex;
flex-direction: column-reverse;
height: 100%;
/**
* fixed positioning is necessary for remote menus and tooltips to pop
* out of the scrolling filmstrip. AtlasKit dialogs and tooltips use
* a library called popper which will position its elements fixed if
* any parent is also fixed.
*/
position: fixed;
z-index: $filmstripVideosZ;
/**
* Hide videos by making them slight to the right.
*/
.filmstrip__videos {
right: 0;
&.hidden {
bottom: auto;
right: -196px;
}
/**
* An id selector is used to match id specificity with existing
* filmstrip styles.
*/
&#remoteVideos {
/**
* Remove horizontal filmstrip padding used to prevent videos
* from overlapping getting near the left-side of the screen.
* An id selector is used to match id specificity with filmstrip
* styles.
*/
padding-left: 0;
transition: right 2s;
}
}
/**
* Re-styles the local Video to better fit vertical filmstrip layout.
*/
#filmstripLocalVideo {
align-self: initial;
bottom: 5px;
display: flex;
flex-direction: column-reverse;
height: auto;
justify-content: flex-start;
}
/**
* Remove unnecssary padding that is normally used to prevent horizontal
* filmstrip from overlapping the left edge of the screen.
*/
#filmstripLocalVideo,
#filmstripRemoteVideos {
padding: 0;
}
#filmstripRemoteVideos {
@include minHWAutoFix();
display: flex;
flex: 1;
flex-direction: column;
height: auto;
justify-content: flex-end;
#filmstripRemoteVideosContainer {
flex-direction: column-reverse;
/**
* Add padding as a hack for Firefox not to show scrollbars when
* unnecessary.
*/
padding: 1px 0;
overflow-x: hidden;
}
}
/**
* Rotate the hide filmstrip icon so it points towards the right edge
* of the screen.
*/
&__toolbar {
transform: rotate(-90deg);
}
/**
* Move the remote video menu trigger to the bottom left of the
* video thumbnail.
*/
.remotevideomenu,
.remote-video-menu-trigger {
bottom: 0;
left: 0;
top: auto;
right: auto;
}
.remote-video-menu-trigger {
margin-bottom: 7px;
}
#remoteVideos {
@include minHWAutoFix();
flex-direction: column;
flex-grow: 1;
}
.videocontainer {
/**
* Move status icons to the bottom right of the thumbnail.
*/
&__toolbar {
text-align: right;
.right {
float: none;
margin: auto;
}
}
}
}
&.filmstrip-only {
.filmstrip {
flex-direction: row-reverse;
}
.filmstrip__videos-filmstripOnly {
height: 100%;
}
}
/**
* These styles are for the video labels that display on the top right. The
* styles adjust the labels' positioning as the filmstrip itself or
* filmstrip's remote videos appear and disappear.
*
* The class with-filmstrip is for when the filmstrip is visible.
* The class without-filmstrip is for when the filmstrip has been toggled to
* be hidden.
* The class opening is for when the filmstrip is transitioning from hidden
* to visible.
*/
.large-video-labels {
&.with-filmstrip {
right: 150px;
}
&.with-filmstrip.opening {
transition: 0.9s;
transition-timing-function: ease-in-out;
}
&.without-filmstrip {
transition: 1.2s ease-in-out;
transition-delay: 0.1s;
}
}
/**
* Apply hardware acceleration to prevent flickering on scroll. The
* selectors are specific to icon wrappers to prevent fixed position dialogs
* and tooltips from getting a new location context due to translate3d.
*/
.connection-indicator,
.remote-video-menu-trigger,
.indicator-icon-container {
transform: translate3d(0, 0, 0);
}
.indicator-container {
float: none;
}
/**
* FIXME: disable pointer to allow any elements moved below to still be
* clickable. The real fix would to make sure those moved elements are
* actually part of the toolbar instead of positioning being faked.
*/
.videocontainer__toolbar {
pointer-events: none;
> div {
pointer-events: none;
}
.toolbar-icon {
pointer-events: all;
}
}
}
/**
* Workarounds for Edge, IE11, and Firefox not handling scrolling properly
* with flex-direction: column-reverse. The remove videos in filmstrip should
* start scrolling from the bottom of the filmstrip, but in those browsers the
* scrolling won't happen. Per W3C spec, scrolling should happen from the
* bottom. As such, use css hacks to get around the css issue, with the intent
* being to remove the hacks as the spec is supported.
*/
@mixin undoColumnReverseVideos() {
.vertical-filmstrip {
#remoteVideos #filmstripRemoteVideos #filmstripRemoteVideosContainer {
flex-direction: column;
}
}
}
/** Firefox detection hack **/
@-moz-document url-prefix() {
@include undoColumnReverseVideos();
}
/** IE11 detection hack **/
@media screen and (-ms-high-contrast: active),
screen and (-ms-high-contrast: none) {
@include undoColumnReverseVideos();
}
/** Edge detection hack **/
@supports (-ms-ime-align:auto) {
@include undoColumnReverseVideos();
}

View File

@@ -627,7 +627,6 @@
padding-left: 10px;
padding-right: 10px;
}
#remotePresenceMessage .no-presence,
#remoteConnectionMessage {
display: none;
}
@@ -740,3 +739,19 @@
visibility: hidden;
}
}
.presence-label {
color: $participantNameColor;
font-size: 12px;
font-weight: 100;
left: 0;
margin: 0 auto;
overflow: hidden;
pointer-events: none;
right: 0;
text-align: center;
text-overflow: ellipsis;
top: calc(50% + 30px);
white-space: nowrap;
width: 100%;
}

View File

@@ -4,15 +4,19 @@ body.welcome-page {
}
.welcome {
background-color: $welcomePageHeaderBackground;
display: flex;
flex-direction: column;
font-family: $welcomePageFontFamily;
height: 100%;
justify-content: space-between;
min-height: 100vh;
position: relative;
.header {
align-items: center;
background: $welcomePageHeaderBackground;
display: flex;
flex-direction: column;
min-height: fit-content;
overflow: hidden;
position: relative;
text-align: center;
@@ -20,49 +24,42 @@ body.welcome-page {
.header-text {
display: flex;
flex-direction: column;
justify-content: space-around;
margin-top: 120px;
margin-bottom: 20px;
margin-top: $watermarkHeight + 80;
margin-bottom: 36px;
max-width: calc(100% - 40px);
min-height: 286px;
width: 645px;
width: 650px;
z-index: $zindex2;
}
.header-text-title {
color: $welcomePageTitleColor;
font-size: 48px;
letter-spacing: -1px;
line-height: 58px;
margin-bottom: 20px;
font-size: 2.5rem;
font-weight: 500;
letter-spacing: 0;
line-height: 1.18;
margin-bottom: 16px;
}
.header-text-description {
color: $welcomePageDescriptionColor;
font-size: 20px;
line-height: 28px;
opacity: 0.8;
font-size: 1rem;
font-weight: 400;
line-height: 24px;
}
.header-image {
background-image: url(../images/welcome_page/curves.png);
background-size: contain;
height: 209px;
position: absolute;
width: 1070px;
}
#new_enter_room {
#enter_room {
align-items: center;
display: flex;
margin-bottom: 20px;
max-width: calc(100% - 40px);
margin-bottom: 20px;
position: relative;
z-index: 2;
width: 650px;
z-index: $zindex2;
.enter-room-input {
display: inline-block;
margin-right: 15px;
width: 350px;
margin-right: 8px;
width: 100%;
}
}
}
@@ -70,24 +67,10 @@ body.welcome-page {
.welcome-page-button {
font-size: 16px;
}
}
.welcome.with-content {
.header {
min-height: 552px;
}
.header-image {
left: -61px;
top: 401px;
}
}
.welcome.without-content {
.header {
.welcome-watermark {
position: absolute;
width: 100%;
height: 100%;
}
.header-image {
bottom: -20px;
left: 0;
}
}

View File

@@ -0,0 +1,31 @@
.filmstrip__toolbar {
@include flex();
flex-direction: column-reverse;
flex-wrap: nowrap;
position: relative;
width: $filmstripToggleButtonWidth;
button {
font-size: 14px;
line-height: 1.2;
text-align: center;
background: transparent;
opacity: 0.7;
height: auto;
width: 100%;
padding: 0;
margin: 0;
border: none;
outline: none;
-webkit-appearance: none;
&:hover {
opacity: 1;
}
i {
cursor: pointer;
}
}
}

View File

@@ -0,0 +1,77 @@
%align-right {
@include flex();
flex-direction: row-reverse;
flex-wrap: nowrap;
justify-content: flex-start;
}
.horizontal-filmstrip .filmstrip {
position: absolute;
bottom: 0;
right: 0;
padding: 10px 5px;
@extend %align-right;
transition: bottom .3s;
z-index: $filmstripVideosZ;
&.reduce-height {
bottom: $newToolbarSizeWithPadding;
}
&__videos {
@extend %align-right;
position:relative;
padding: 0;
/* The filmstrip should not be covered by the left toolbar. */
bottom: 0;
width:auto;
overflow: visible !important;
&#remoteVideos {
border: $thumbnailsBorder solid transparent;
padding-left: $defaultToolbarSize + 5;
transition: bottom 2s;
}
/**
* The local video identifier.
*/
&#filmstripLocalVideo {
align-self: flex-end;
display: block;
}
&.hidden {
bottom: calc(-196px - #{$newToolbarSizeWithPadding});
}
.remote-videos-container {
display: flex;
}
}
/**
* Style the filmstrip videos in filmstrip-only mode.
*/
&__videos-filmstripOnly {
margin-top: auto;
margin-bottom: auto;
.filmstrip__videos {
&#filmstripLocalVideo {
bottom: 0px;
}
}
}
.remote-videos-container {
transition: opacity 1s;
}
&.hide-videos {
.remote-videos-container {
opacity: 0;
pointer-events: none;
}
}
}

View File

@@ -0,0 +1,56 @@
.filmstrip__videos .videocontainer {
display: none;
position: relative;
background-size: contain;
border: $thumbnailVideoBorder solid transparent;
border-radius: $borderRadius;
margin: 0 $thumbnailVideoMargin;
&.videoContainerFocused, &:hover {
cursor: hand;
}
/**
* Focused video thumbnail.
*/
&.videoContainerFocused {
transition-duration: 0.5s;
-webkit-transition-duration: 0.5s;
-webkit-animation-name: greyPulse;
-webkit-animation-duration: 2s;
-webkit-animation-iteration-count: 1;
border: $thumbnailVideoBorder solid $videoThumbnailSelected !important;
box-shadow: inset 0 0 3px $videoThumbnailSelected,
0 0 3px $videoThumbnailSelected !important;
}
.remotevideomenu > .icon-menu {
display: none;
}
/**
* Hovered video thumbnail.
*/
&:hover {
cursor: hand;
border: $thumbnailVideoBorder solid $videoThumbnailHovered;
box-shadow: inset 0 0 3px $videoThumbnailHovered,
0 0 3px $videoThumbnailHovered;
.remotevideomenu > .icon-menu {
display: inline-block;
}
}
& > video {
cursor: hand;
border-radius: $borderRadius;
object-fit: cover;
overflow: hidden;
}
.presence-label {
position: absolute;
z-index: $zindex3;
}
}

View File

@@ -0,0 +1,171 @@
.vertical-filmstrip .filmstrip {
&.hide-videos {
.remote-videos-container {
opacity: 0;
pointer-events: none;
}
}
/*
* Firefox sets flex items to min-height: auto and min-width: auto,
* preventing flex children from shrinking like they do on other browsers.
* Setting min-height and min-width 0 is a workaround for the issue so
* Firefox behaves like other browsers.
* https://bugzilla.mozilla.org/show_bug.cgi?id=1043520
*/
@mixin minHWAutoFix() {
min-height: 0;
min-width: 0;
}
@extend %align-right;
align-items: flex-end;
bottom: 0;
box-sizing: border-box;
display: flex;
flex-direction: column-reverse;
height: 100%;
padding: 10px 5px;
/**
* fixed positioning is necessary for remote menus and tooltips to pop
* out of the scrolling filmstrip. AtlasKit dialogs and tooltips use
* a library called popper which will position its elements fixed if
* any parent is also fixed.
*/
position: fixed;
top: 0;
transition: height .3s ease-in;
right: 0;
z-index: $filmstripVideosZ;
&.reduce-height {
height: calc(100% - #{$newToolbarSizeWithPadding});
}
/**
* Hide videos by making them slight to the right.
*/
.filmstrip__videos {
@extend %align-right;
bottom: 0;
overflow: visible !important;
padding: 0;
position:relative;
right: 0;
width: auto;
&.hidden {
bottom: auto;
right: -196px;
}
/**
* An id selector is used to match id specificity with existing
* filmstrip styles.
*/
&#remoteVideos {
border: $thumbnailsBorder solid transparent;
padding-left: 0;
transition: right 2s;
}
}
/**
* Re-styles the local Video to better fit vertical filmstrip layout.
*/
#filmstripLocalVideo {
align-self: initial;
bottom: 5px;
display: flex;
flex-direction: column-reverse;
height: auto;
justify-content: flex-start;
}
/**
* Remove unnecssary padding that is normally used to prevent horizontal
* filmstrip from overlapping the left edge of the screen.
*/
#filmstripLocalVideo,
#filmstripRemoteVideos {
padding: 0;
}
#filmstripRemoteVideos {
@include minHWAutoFix();
display: flex;
flex: 1;
flex-direction: column;
height: auto;
justify-content: flex-end;
#filmstripRemoteVideosContainer {
flex-direction: column-reverse;
/**
* Add padding as a hack for Firefox not to show scrollbars when
* unnecessary.
*/
padding: 1px 0;
overflow-x: hidden;
}
}
#remoteVideos {
@include minHWAutoFix();
flex-direction: column;
flex-grow: 1;
}
.remote-videos-container {
display: flex;
transition: opacity 1s;
}
}
/**
* Override other styles to support vertical filmstrip mode.
*/
.filmstrip-only .vertical-filmstrip {
.filmstrip {
flex-direction: row-reverse;
}
.filmstrip__videos-filmstripOnly {
margin-top: auto;
margin-bottom: auto;
height: 100%;
}
.filmstrip__videos {
&#filmstripLocalVideo {
bottom: 0px;
}
}
}
/**
* Workarounds for Edge and Firefox not handling scrolling properly with
* flex-direction: column-reverse. The remove videos in filmstrip should
* start scrolling from the bottom of the filmstrip, but in those browsers the
* scrolling won't happen. Per W3C spec, scrolling should happen from the
* bottom. As such, use css hacks to get around the css issue, with the intent
* being to remove the hacks as the spec is supported.
*/
@mixin undoColumnReverseVideos() {
.vertical-filmstrip {
#remoteVideos #filmstripRemoteVideos #filmstripRemoteVideosContainer {
flex-direction: column;
}
}
}
/** Firefox detection hack **/
@-moz-document url-prefix() {
@include undoColumnReverseVideos();
}
/** Edge detection hack **/
@supports (-ms-ime-align:auto) {
@include undoColumnReverseVideos();
}

View File

@@ -0,0 +1,105 @@
/**
* Rotate the hide filmstrip icon so it points towards the right edge
* of the screen.
*/
.vertical-filmstrip .filmstrip__toolbar {
transform: rotate(-90deg);
}
/**
* Overrides for video containers that should not be centered aligned to avoid=
* clashing with the filmstrip.
*/
.vertical-filmstrip #etherpad,
.vertical-filmstrip #sharedvideo {
text-align: left;
}
/**
* Overrides for small videos in vertical filmstrip mode.
*/
.vertical-filmstrip .filmstrip__videos .videocontainer {
/**
* Move status icons to the bottom right of the thumbnail.
*/
.videocontainer__toolbar {
/**
* FIXME: disable pointer to allow any elements moved below to still
* be clickable. The real fix would to make sure those moved elements
* are actually part of the toolbar instead of positioning being faked.
*/
pointer-events: none;
text-align: right;
> div {
pointer-events: none;
}
.right {
float: none;
margin: auto;
}
.toolbar-icon {
pointer-events: all;
}
}
/**
* Apply hardware acceleration to prevent flickering on scroll. The
* selectors are specific to icon wrappers to prevent fixed position dialogs
* and tooltips from getting a new location context due to translate3d.
*/
.connection-indicator,
.remote-video-menu-trigger,
.indicator-icon-container {
transform: translate3d(0, 0, 0);
}
.indicator-container {
float: none;
}
/**
* Move the remote video menu trigger to the bottom left of the video
* thumbnail.
*/
.remotevideomenu,
.remote-video-menu-trigger {
bottom: 0;
left: 0;
top: auto;
right: auto;
}
.remote-video-menu-trigger {
margin-bottom: 7px;
}
}
/**
* Overrides for quality labels in filmstrip only mode. The styles adjust the
* labels' positioning as the filmstrip itself or filmstrip's remote videos
* appear and disappear.
*
* The class with-filmstrip is for when the filmstrip is visible.
* The class without-filmstrip is for when the filmstrip has been toggled to
* be hidden.
* The class opening is for when the filmstrip is transitioning from hidden
* to visible.
*/
.vertical-filmstrip .large-video-labels {
&.with-filmstrip {
right: 150px;
}
&.with-filmstrip.opening {
transition: 0.9s;
transition-timing-function: ease-in-out;
}
&.without-filmstrip {
transition: 1.2s ease-in-out;
transition-delay: 0.1s;
}
}

View File

@@ -31,6 +31,7 @@
/* Modules BEGIN */
@import 'aui_reset';
@import 'atlaskit_overrides';
@import 'base';
@import 'utils';
@import 'overlay/overlay';
@@ -41,6 +42,7 @@
@import 'modals/dialog';
@import 'modals/feedback/feedback';
@import 'modals/invite/info';
@import 'modals/settings/settings';
@import 'modals/speaker_stats/speaker_stats';
@import 'modals/video-quality/video-quality';
@import 'videolayout_default';
@@ -67,10 +69,14 @@
@import '404';
@import 'policy';
@import 'popover';
@import 'filmstrip';
@import 'filmstrip/filmstrip_toolbar';
@import 'filmstrip/horizontal_filmstrip';
@import 'filmstrip/small_video';
@import 'filmstrip/vertical_filmstrip';
@import 'filmstrip/vertical_filmstrip_overrides';
@import 'unsupported-browser/main';
@import 'modals/invite/add-people';
@import 'vertical_filmstrip_overrides';
@import 'deep-linking/main';
@import 'transcription-subtitles';
@import 'navigate_section_list';
/* Modules END */

View File

@@ -103,10 +103,14 @@
font-size: 14px;
a {
color: 4C9AFF;
color: #2684FF;
cursor: pointer;
text-decoration: none;
}
a:hover {
color: #B3D4FF;
}
}
.audio-input-preview {

View File

@@ -1,10 +1,7 @@
.use-new-toolbox {
font-size: 14px;
}
.info-dialog {
cursor: default;
display: flex;
font-size: 14px;
.info-dialog-action-link {
display: inline-block;
@@ -129,11 +126,16 @@
.dial-in-page {
align-items: center;
box-sizing: border-box;
display: flex;
flex-direction: column;
font-size: 24px;
height: 100%;
justify-content: center;
max-height: 100%;
overflow: auto;
padding: 25px;
position: absolute;
transform: translateY(-50%);
top: 50%;
width: 100%;
.dial-in-numbers-list {
@@ -143,6 +145,7 @@
.dial-in-conference-id {
text-align: center;
min-width: 200px;
width: 30%;
}
}

View File

@@ -0,0 +1,43 @@
.settings-pane {
display: flex;
width: 100%;
&.profile-pane {
flex-direction: column;
}
.auth-name {
margin-bottom: 4px;
}
.device-selection {
margin-top: 20px;
}
.mock-atlaskit-label {
color: #56637A;
font-size: 12px;
font-weight: 600;
line-height: 1.33;
padding: 20px 0px 4px 0px;
}
.more-tab,
.profile-edit {
display: flex;
width: 100%;
}
.profile-edit-field,
.settings-sub-pane {
flex: 1;
}
.profile-edit-field {
margin-right: 20px;
}
.language-settings {
max-width: 50%;
}
}

View File

@@ -155,7 +155,6 @@
.circular-label {
color: white;
font-family: -apple-system, BlinkMacSystemFont, $baseFontFamily;
font-weight: bold;
margin-left: 8px;
opacity: 0.8;

View File

@@ -6,12 +6,10 @@
height: 100%;
position: fixed;
z-index: $ringingZ;
background: linear-gradient(transparent, #000);
opacity: 0.8;
@include transparentBg(#283447, 0.95);
&.solidBG {
background: $defaultBackground;
opacity: 1;
}
&__content {
@@ -22,20 +20,26 @@
top: 50%;
margin-left: -200px;
margin-top: -125px;
font-weight: 400;
font-size: 14px;
text-align: center;
font-weight: normal;
color: #FFFFFF;
}
&__avatar {
width: 100px;
height: 100px;
width: 128px;
height: 128px;
border-radius: 50%;
border: 2px solid #1B2638;
}
&__caller-info {
.mention {
color: #333;
}
&__status{
margin-top: 15px;
font-size: 14px;
line-height: 20px;
}
&__name {
font-size: 24px;
line-height: 32px;
}
}

View File

@@ -194,6 +194,15 @@ changes. The listener will receive an object with the following structure:
}
```
* **emailChange** - event notifications about email
changes. The listener will receive an object with the following structure:
```javascript
{
"id": id, // the id of the participant that changed his email
"email": email // the new email
}
```
* **participantJoined** - event notifications about new participants who join the room. The listener will receive an object with the following structure:
```javascript
{
@@ -290,6 +299,11 @@ You can get the display name of a participant in the conference with the followi
var displayName = api.getDisplayName(participantId);
```
You can get the email of a participant in the conference with the following API function:
```javascript
var email = api.getEmail(participantId);
```
You can get the iframe HTML element where Jitsi Meet is loaded with the following API function:
```javascript
var iframe = api.getIFrame();

View File

@@ -13,7 +13,7 @@ There are also some complete [example config files](https://github.com/jitsi/jit
## Network description
This how the network look like:
This is how the network looks:
```
+ +
| |

View File

@@ -4,7 +4,11 @@ This document describes the required steps for a quick Jitsi Meet installation o
Debian Wheezy and other older systems may require additional things to be done. Specifically for Wheezy, [libc needs to be updated](http://lists.jitsi.org/pipermail/users/2015-September/010064.html).
N.B.: All commands are supposed to be run by root. If you are logged in as a regular user with sudo rights, please prepend ___sudo___ to each of the commands.
N.B.:
a.) All commands are supposed to be run by root. If you are logged in as a regular user with sudo rights, please prepend ___sudo___ to each of the commands.
b.) You only need to do this if you want to ___host your own Jitsi server___. If you just want to have a video conference with someone, use https://meet.jit.si instead.
## Basic Jitsi Meet install
@@ -59,6 +63,8 @@ org.ice4j.ice.harvest.NAT_HARVESTER_PUBLIC_ADDRESS=<Public.IP.Address>
See [the documenation of ice4j](https://github.com/jitsi/ice4j/blob/master/doc/configuration.md)
for details.
By default, anyone who has access to your jitsi instance will be able to start a conferencee: if your server is open to the world, anyone can have a chat with anyone else. If you want to limit the ability to start a conference to registered users, set up a "secure domain". Follow the instructions at https://github.com/jitsi/jicofo#secure-domain.
### Open a conference
Launch a web browser (Chrome, Chromium or latest Opera) and enter in the URL bar the hostname (or IP address) you used in the previous step.

View File

@@ -1,6 +1,6 @@
One-to-one calls should avoid going throught the JVB for optimal performance and for optimal resource usage. This is why we've added the peer-to-peer mode where the two participants connect directly to each other. Unfortunately, a direct connection is not always possible between the participants. In those cases you can use a TURN server to relay the traffic (n.b. the JVB does much more than just relay the traffic, so this is not the same as using the JVB to "relay" the traffic).
This document describes how to enable TURN server support in one-to-one calls in Jitsi Meet. Even tho it gives some hints how to configure [prosody](prosody.im) and [coTURN](https://github.com/coturn/coturn), it assumes a properly configured TURN server and a proprely configured XMPP server.
This document describes how to enable TURN server support in one-to-one calls in Jitsi Meet, even though it gives some hints how to configure [prosody](prosody.im) and [coTURN](https://github.com/coturn/coturn), it assumes a properly configured TURN server and a proprely configured XMPP server.
One way to configure TURN support in meet with a static configuration. You can simply fill out the `p2p.stunServers` option with appropriate values, e.g.:

Binary file not shown.

View File

@@ -15,6 +15,7 @@
<glyph unicode="&#xe409;" glyph-name="navigate_next" d="M426 768l256-256-256-256-60 60 196 196-196 196z" />
<glyph unicode="&#xe425;" glyph-name="timer" d="M512 170c166 0 298 134 298 300s-132 298-298 298-298-132-298-298 132-300 298-300zM812 708c52-66 84-148 84-238 0-212-172-384-384-384s-384 172-384 384 172 384 384 384c90 0 174-34 240-86l60 62c22-18 42-38 60-60zM470 426v256h84v-256h-84zM640 982v-86h-256v86h256z" />
<glyph unicode="&#xe5c4;" glyph-name="arrow_back" d="M854 554v-84h-520l238-240-60-60-342 342 342 342 60-60-238-240h520z" />
<glyph unicode="&#xe5cd;" glyph-name="close" d="M810 750l-238-238 238-238-60-60-238 238-238-238-60 60 238 238-238 238 60 60 238-238 238 238z" />
<glyph unicode="&#xe5d2;" glyph-name="menu" d="M128 768h768v-86h-768v86zM128 470v84h768v-84h-768zM128 256v86h768v-86h-768z" />
<glyph unicode="&#xe5d4;" glyph-name="thumb-menu" d="M512 342c46 0 86-40 86-86s-40-86-86-86-86 40-86 86 40 86 86 86zM512 598c46 0 86-40 86-86s-40-86-86-86-86 40-86 86 40 86 86 86zM512 682c-46 0-86 40-86 86s40 86 86 86 86-40 86-86-40-86-86-86z" />
<glyph unicode="&#xe603;" glyph-name="presentation" horiz-adv-x="1088" d="M952.495 1019.065h-818.689c-72.81 0-132.183-60.63-132.183-135.162v-750.719c0-74.473 59.372-135.101 132.183-135.101h818.686c72.936 0 132.314 60.625 132.314 135.101v750.722c0.003 74.532-59.378 135.159-132.311 135.159zM946.346 139.651h-806.14v737.822h806.015l0.126-737.822zM685.753 738.544h216.911v-566.758h-216.911v566.758zM428.672 610.002h216.911v-438.216h-216.911v438.216zM172.339 481.46h216.161v-309.677h-216.161v309.677z" />
@@ -70,4 +71,8 @@
<glyph unicode="&#xe92a;" glyph-name="camera-take-picture" d="M725.333 512c0-117.821-95.513-213.333-213.333-213.333s-213.333 95.513-213.333 213.333c0 117.821 95.513 213.333 213.333 213.333s213.333-95.513 213.333-213.333zM512 256c141.385 0 256 114.615 256 256s-114.615 256-256 256v0c-141.385 0-256-114.615-256-256s114.615-256 256-256v0zM512 213.333c-164.949 0-298.667 133.718-298.667 298.667s133.718 298.667 298.667 298.667v0c164.949 0 298.667-133.718 298.667-298.667s-133.718-298.667-298.667-298.667v0z" />
<glyph unicode="&#xe92b;" glyph-name="rec" d="M512 0c-282.77 0-512 229.23-512 512s229.23 512 512 512c282.77 0 512-229.23 512-512s-229.23-512-512-512zM581.333 433.782h-110.595v59.233h104.338v40.332h-104.338v56.87h110.595v43.539h-161.665v-243.512h161.665v43.539zM738.771 384c58.849 0 101.802 36.282 106.029 88.933h-49.717c-4.904-26.832-26.888-44.045-56.143-44.045-38.556 0-62.4 31.895-62.4 83.196s23.844 83.027 62.231 83.027c29.086 0 51.239-18.394 56.143-46.407h49.717c-3.72 52.989-48.026 91.296-105.86 91.296-70.855 0-114.485-48.77-114.485-127.916 0-79.314 43.798-128.084 114.485-128.084zM230.27 478.502h41.769l45.489-88.258h57.834l-51.408 96.19c28.072 11.138 44.306 38.138 44.306 69.189 0 48.432-32.976 78.133-86.582 78.133h-102.478v-243.512h51.070v88.258zM230.27 592.58v-74.927h44.813c25.704 0 40.754 13.838 40.754 37.295 0 23.119-15.896 37.632-41.262 37.632h-44.306z" />
<glyph unicode="&#xe92c;" glyph-name="live" d="M512 0c-282.77 0-512 229.23-512 512s229.23 512 512 512c282.77 0 512-229.23 512-512s-229.23-512-512-512zM298.225 430.025h-112.35v206.5h-52.85v-252.525h165.2v46.025zM399.025 384v252.525h-52.85v-252.525h52.85zM591.525 384l84.175 252.525h-56.875l-56.35-193.025h-3.15l-57.4 193.025h-59.675l85.4-252.525h63.875zM886.050 429.15h-114.45v61.425h107.975v41.825h-107.975v58.975h114.45v45.15h-167.3v-252.525h167.3v45.15z" />
<glyph unicode="&#xe92d;" glyph-name="speaker" d="M0 512c0-282.795 229.205-512 512-512s512 229.205 512 512c0 282.795-229.205 512-512 512s-512-229.205-512-512zM525.005 759.362c-20.475 24.944-16.326 61.342 9.268 81.297s62.94 15.911 83.416-9.033c16.036-19.536 38.593-52.97 60.894-97.797 81.621-164.065 89.461-340.992-26.857-506.352-8.384-11.919-17.386-23.69-27.012-35.307-20.593-24.851-57.959-28.727-83.458-8.657s-29.476 56.487-8.882 81.338c7.686 9.275 14.833 18.621 21.455 28.035 88.66 126.041 82.71 260.306 17.953 390.475-10.599 21.305-21.94 40.51-33.198 57.196-6.515 9.657-11.322 16.057-13.578 18.805zM353.479 647.46c-19.353 24.679-15.129 60.448 9.434 79.893s60.164 15.2 79.517-9.479c9.635-12.287 22.577-32.644 35.209-60.034 50.35-109.176 50.35-231.689-33.639-349.612-18.198-25.551-53.566-31.441-78.997-13.157s-31.294 53.819-13.096 79.37c57.564 80.822 57.564 160.581 22.983 235.565-8.601 18.65-16.892 31.691-21.412 37.455z" />
<glyph unicode="&#xe92e;" glyph-name="tiles-many" d="M113.778 1024h227.556c62.838 0 113.778-50.94 113.778-113.778v-227.556c0-62.838-50.94-113.778-113.778-113.778h-227.556c-62.838 0-113.778 50.94-113.778 113.778v227.556c0 62.838 50.94 113.778 113.778 113.778zM170.667 910.222c-31.419 0-56.889-25.47-56.889-56.889v-113.778c0-31.419 25.47-56.889 56.889-56.889h113.778c31.419 0 56.889 25.47 56.889 56.889v113.778c0 31.419-25.47 56.889-56.889 56.889h-113.778zM113.778 455.111h227.556c62.838 0 113.778-50.94 113.778-113.778v-227.556c0-62.838-50.94-113.778-113.778-113.778h-227.556c-62.838 0-113.778 50.94-113.778 113.778v227.556c0 62.838 50.94 113.778 113.778 113.778zM170.667 341.333c-31.419 0-56.889-25.47-56.889-56.889v-113.778c0-31.419 25.47-56.889 56.889-56.889h113.778c31.419 0 56.889 25.47 56.889 56.889v113.778c0 31.419-25.47 56.889-56.889 56.889h-113.778zM682.667 1024h227.556c62.838 0 113.778-50.94 113.778-113.778v-227.556c0-62.838-50.94-113.778-113.778-113.778h-227.556c-62.838 0-113.778 50.94-113.778 113.778v227.556c0 62.838 50.94 113.778 113.778 113.778zM739.556 910.222c-31.419 0-56.889-25.47-56.889-56.889v-113.778c0-31.419 25.47-56.889 56.889-56.889h113.778c31.419 0 56.889 25.47 56.889 56.889v113.778c0 31.419-25.47 56.889-56.889 56.889h-113.778zM682.667 455.111h227.556c62.838 0 113.778-50.94 113.778-113.778v-227.556c0-62.838-50.94-113.778-113.778-113.778h-227.556c-62.838 0-113.778 50.94-113.778 113.778v227.556c0 62.838 50.94 113.778 113.778 113.778zM739.556 341.333c-31.419 0-56.889-25.47-56.889-56.889v-113.778c0-31.419 25.47-56.889 56.889-56.889h113.778c31.419 0 56.889 25.47 56.889 56.889v113.778c0 31.419-25.47 56.889-56.889 56.889h-113.778z" />
<glyph unicode="&#xe92f;" glyph-name="tiles-one" d="M170.667 810.667h682.667c47.128 0 85.333-38.205 85.333-85.333v-426.667c0-47.128-38.205-85.333-85.333-85.333h-682.667c-47.128 0-85.333 38.205-85.333 85.333v426.667c0 47.128 38.205 85.333 85.333 85.333zM213.333 725.333c-23.564 0-42.667-19.103-42.667-42.667v-341.333c0-23.564 19.103-42.667 42.667-42.667h597.333c23.564 0 42.667 19.103 42.667 42.667v341.333c0 23.564-19.103 42.667-42.667 42.667h-597.333z" />
<glyph unicode="&#xe930;" glyph-name="closed_caption" d="M768 554v44c0 24-18 42-42 42h-128c-24 0-44-18-44-42v-172c0-24 20-42 44-42h128c24 0 42 18 42 42v44h-64v-22h-86v128h86v-22h64zM470 554v44c0 24-20 42-44 42h-128c-24 0-42-18-42-42v-172c0-24 18-42 42-42h128c24 0 44 18 44 42v44h-64v-22h-86v128h86v-22h64zM810 854c46 0 86-40 86-86v-512c0-46-40-86-86-86h-596c-48 0-86 40-86 86v512c0 46 38 86 86 86h596z" />
</font></defs></svg>

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

View File

@@ -1,26 +1,2 @@
// The type field of react-native application loader's React Element is created
// as number and not Symbol, because it's not been defined by the polyfill yet.
// We import the application renderer, before Symbol is defined, in order to use
// number types as well. Otherwise this will result in the invariant exception,
// because fiber thingy will not recognise root react-native component as React
// Element, but as an Object.
//
// See node_modules/react-native/Libraries/polyfills/babelHelpers.js
// :babelHelpers.createRawReactElement - that's where first react-native element
// is created (super early - it's the app loader).
//
// See node_modules/react-native/Libraries/Renderer/ReactNativeFiber-dev.js
// and look for REACT_ELEMENT_TYPE definition - it's defined later when Symbol
// has been defined and type will not match.
//
// As an alternative solution we could stop using/polyfilling Symbols and
// replace with classpath string constants or some kind of a wrapper around
// that.
import 'react-native/Libraries/ReactNative/renderApplication';
// Android doesn't provide Symbol
import 'es6-symbol/implement';
import './react/index.native';

View File

@@ -5,6 +5,12 @@ var interfaceConfig = {
// methods allowing to use variables both in css and js.
DEFAULT_BACKGROUND: '#474747',
/**
* Whether or not the blurred video background for large video should be
* displayed on browsers that can support it.
*/
DISABLE_VIDEO_BACKGROUND: false,
INITIAL_TOOLBAR_TIMEOUT: 20000,
TOOLBAR_TIMEOUT: 4000,
TOOLBAR_ALWAYS_VISIBLE: false,
@@ -39,13 +45,13 @@ var interfaceConfig = {
* jwt.
*/
TOOLBAR_BUTTONS: [
'microphone', 'camera', 'desktop', 'fullscreen', 'fodeviceselection', 'hangup',
'profile', 'info', 'chat', 'recording', 'livestreaming', 'etherpad',
'sharedvideo', 'settings', 'raisehand', 'videoquality', 'filmstrip',
'invite', 'feedback', 'stats', 'shortcuts'
'microphone', 'camera', 'closedcaptions', 'desktop', 'fullscreen',
'fodeviceselection', 'hangup', 'profile', 'info', 'chat', 'recording',
'livestreaming', 'etherpad', 'sharedvideo', 'settings', 'raisehand',
'videoquality', 'filmstrip', 'invite', 'feedback', 'stats', 'shortcuts'
],
SETTINGS_SECTIONS: [ 'language', 'devices', 'moderator' ],
SETTINGS_SECTIONS: [ 'devices', 'language', 'moderator', 'profile' ],
// Determines how the video would fit the screen. 'both' would fit the whole
// screen, 'height' would fit the original video height to the height of the
@@ -74,6 +80,14 @@ var interfaceConfig = {
DISABLE_FOCUS_INDICATOR: false,
DISABLE_DOMINANT_SPEAKER_INDICATOR: false,
/**
* Whether the speech to text transcription subtitles panel is disabled.
* If {@code undefined}, defaults to {@code false}.
*
* @type {boolean}
*/
DISABLE_TRANSCRIPTION_SUBTITLES: false,
/**
* Whether the ringing sound in the call/ring overlay is disabled. If
* {@code undefined}, defaults to {@code false}.
@@ -131,6 +145,13 @@ var interfaceConfig = {
*/
CONNECTION_INDICATOR_AUTO_HIDE_TIMEOUT: 5000,
/**
* If true, hides the connection indicators completely.
*
* @type {boolean}
*/
CONNECTION_INDICATOR_DISABLED: false,
/**
* The name of the application connected to the "Add people" search service.
*/
@@ -145,14 +166,11 @@ var interfaceConfig = {
VIDEO_QUALITY_LABEL_DISABLED: false,
/**
* Temporary feature flag to debug performance with the large video
* background blur. On initial implementation, blur was always enabled so a
* falsy value here will be used to keep blur enabled, as will the value
* "video", and will render the blur over a video element. The value
* "canvas" will display the blur over a canvas element, while the value
* "off" will prevent the background from rendering.
* If true, will display recent list
*
* @type {boolean}
*/
_BACKGROUND_BLUR: 'canvas'
RECENT_LIST_ENABLED: true
/**
* Specify custom URL for downloading android mobile app.

View File

@@ -28,13 +28,15 @@ target 'JitsiMeet' do
pod 'react-native-background-timer',
:path => '../node_modules/react-native-background-timer'
pod 'react-native-fetch-blob',
:path => '../node_modules/react-native-fetch-blob'
pod 'react-native-fast-image',
:path => '../node_modules/react-native-fast-image'
pod 'react-native-keep-awake',
:path => '../node_modules/react-native-keep-awake'
pod 'react-native-locale-detector',
:path => '../node_modules/react-native-locale-detector'
pod 'react-native-webrtc', :path => '../node_modules/react-native-webrtc'
pod 'ReactNativePermissions',
:path => '../node_modules/react-native-permissions'
pod 'RNSound', :path => '../node_modules/react-native-sound'
pod 'RNVectorIcons', :path => '../node_modules/react-native-vector-icons'
pod 'react-native-calendar-events',

View File

@@ -1,6 +1,7 @@
PODS:
- boost-for-react-native (1.63.0)
- DoubleConversion (1.1.5)
- FLAnimatedImage (1.0.12)
- Folly (2016.09.26.00):
- boost-for-react-native
- DoubleConversion
@@ -12,8 +13,11 @@ PODS:
- React
- react-native-calendar-events (1.6.0):
- React
- react-native-fetch-blob (0.10.6):
- React/Core
- react-native-fast-image (4.0.14):
- FLAnimatedImage
- React
- SDWebImage/Core
- SDWebImage/GIF
- react-native-keep-awake (2.0.6):
- React
- react-native-locale-detector (1.0.0):
@@ -59,6 +63,8 @@ PODS:
- React/Core
- React/fishhook
- React/RCTBlob
- ReactNativePermissions (1.1.1):
- React
- RNSound (0.10.9):
- React/Core
- RNSound/Core (= 0.10.9)
@@ -66,6 +72,10 @@ PODS:
- React/Core
- RNVectorIcons (4.4.2):
- React
- SDWebImage/Core (4.4.2)
- SDWebImage/GIF (4.4.2):
- FLAnimatedImage (~> 1.0)
- SDWebImage/Core
- yoga (0.55.4.React)
DEPENDENCIES:
@@ -74,7 +84,7 @@ DEPENDENCIES:
- glog (from `../node_modules/react-native/third-party-podspecs/glog.podspec`)
- react-native-background-timer (from `../node_modules/react-native-background-timer`)
- react-native-calendar-events (from `../node_modules/react-native-calendar-events`)
- react-native-fetch-blob (from `../node_modules/react-native-fetch-blob`)
- react-native-fast-image (from `../node_modules/react-native-fast-image`)
- react-native-keep-awake (from `../node_modules/react-native-keep-awake`)
- react-native-locale-detector (from `../node_modules/react-native-locale-detector`)
- react-native-webrtc (from `../node_modules/react-native-webrtc`)
@@ -88,54 +98,66 @@ DEPENDENCIES:
- React/RCTNetwork (from `../node_modules/react-native`)
- React/RCTText (from `../node_modules/react-native`)
- React/RCTWebSocket (from `../node_modules/react-native`)
- ReactNativePermissions (from `../node_modules/react-native-permissions`)
- RNSound (from `../node_modules/react-native-sound`)
- RNVectorIcons (from `../node_modules/react-native-vector-icons`)
- yoga (from `../node_modules/react-native/ReactCommon/yoga`)
SPEC REPOS:
https://github.com/cocoapods/specs.git:
- boost-for-react-native
- FLAnimatedImage
- SDWebImage
EXTERNAL SOURCES:
DoubleConversion:
:podspec: ../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec
:podspec: "../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec"
Folly:
:podspec: ../node_modules/react-native/third-party-podspecs/Folly.podspec
:podspec: "../node_modules/react-native/third-party-podspecs/Folly.podspec"
glog:
:podspec: ../node_modules/react-native/third-party-podspecs/glog.podspec
:podspec: "../node_modules/react-native/third-party-podspecs/glog.podspec"
React:
:path: ../node_modules/react-native
:path: "../node_modules/react-native"
react-native-background-timer:
:path: ../node_modules/react-native-background-timer
:path: "../node_modules/react-native-background-timer"
react-native-calendar-events:
:path: ../node_modules/react-native-calendar-events
react-native-fetch-blob:
:path: ../node_modules/react-native-fetch-blob
:path: "../node_modules/react-native-calendar-events"
react-native-fast-image:
:path: "../node_modules/react-native-fast-image"
react-native-keep-awake:
:path: ../node_modules/react-native-keep-awake
:path: "../node_modules/react-native-keep-awake"
react-native-locale-detector:
:path: ../node_modules/react-native-locale-detector
:path: "../node_modules/react-native-locale-detector"
react-native-webrtc:
:path: ../node_modules/react-native-webrtc
:path: "../node_modules/react-native-webrtc"
ReactNativePermissions:
:path: "../node_modules/react-native-permissions"
RNSound:
:path: ../node_modules/react-native-sound
:path: "../node_modules/react-native-sound"
RNVectorIcons:
:path: ../node_modules/react-native-vector-icons
:path: "../node_modules/react-native-vector-icons"
yoga:
:path: ../node_modules/react-native/ReactCommon/yoga
:path: "../node_modules/react-native/ReactCommon/yoga"
SPEC CHECKSUMS:
boost-for-react-native: 39c7adb57c4e60d6c5479dd8623128eb5b3f0f2c
DoubleConversion: e22e0762848812a87afd67ffda3998d9ef29170c
FLAnimatedImage: 4a0b56255d9b05f18b6dd7ee06871be5d3b89e31
Folly: 211775e49d8da0ca658aebc8eab89d642935755c
glog: 1de0bb937dccdc981596d3b5825ebfb765017ded
React: aa2040dbb6f317b95314968021bd2888816e03d5
react-native-background-timer: 63dcbf37dbcf294b5c6c071afcdc661fa06a7594
react-native-calendar-events: fe6fbc8ed337a7423c98f2c9012b25f20444de09
react-native-fetch-blob: 63394b1d7b0781547b3e4463b3195790177b1222
react-native-fast-image: cba3d9bf9c2cf8ddb643d887a686c53a5dd90a2c
react-native-keep-awake: 0de4bd66de0c23178107dce0c2fcc3354b2a8e94
react-native-locale-detector: d1b2c6fe5abb56e3a1efb6c2d6f308c05c4251f1
react-native-webrtc: 31b6d3f1e3e2ce373aa43fd682b04367250f807d
ReactNativePermissions: 9f2d9c45c98800795e6c2ed330e25d11a66a8169
RNSound: b360b3862d3118ed1c74bb9825696b5957686ac4
RNVectorIcons: c0dbfbf6068fefa240c37b0f71bd03b45dddac44
SDWebImage: 624d6e296c69b244bcede364c72ae0430ac14681
yoga: a23273df0088bf7f2bb7e5d7b00044ea57a2a54a
PODFILE CHECKSUM: fb12a5ae406b901e95aeb1ab5ebbb02773c46ede
PODFILE CHECKSUM: 1d5c8382f73d9540fac68d93b32e1d3b58d069ee
COCOAPODS: 1.4.0
COCOAPODS: 1.5.3

View File

@@ -56,13 +56,13 @@
</dict>
</dict>
<key>NSCalendarsUsageDescription</key>
<string>See your scheduled conferences in the app.</string>
<string>See your scheduled meetings in the app.</string>
<key>NSCameraUsageDescription</key>
<string>Participate in conferences with video.</string>
<string>Participate in meetings with video.</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string></string>
<key>NSMicrophoneUsageDescription</key>
<string>Participate in conferences with voice.</string>
<string>Participate in meetings with voice.</string>
<key>UIBackgroundModes</key>
<array>
<string>audio</string>

View File

@@ -20,6 +20,9 @@
#import <React/RCTLog.h>
@interface AudioMode : NSObject<RCTBridgeModule>
@property(nonatomic, strong) dispatch_queue_t workerQueue;
@end
@implementation AudioMode {
@@ -52,15 +55,18 @@ typedef enum {
if (self) {
_category = nil;
_mode = nil;
dispatch_queue_attr_t attributes =
dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL,
QOS_CLASS_USER_INITIATED, -1);
_workerQueue = dispatch_queue_create("WebRTCModule.queue", attributes);
}
return self;
}
- (dispatch_queue_t)methodQueue {
// Make sure all our methods run in the main thread. The route change
// notification runs there so this will make sure it will only be fired
// after our changes have been applied (when we cause them, that is).
return dispatch_get_main_queue();
// Use a dedicated queue for audio mode operations.
return _workerQueue;
}
- (void)routeChanged:(NSNotification*)notification {
@@ -70,12 +76,15 @@ typedef enum {
integerValue];
switch (reason) {
case AVAudioSessionRouteChangeReasonCategoryChange:
case AVAudioSessionRouteChangeReasonCategoryChange: {
// The category has changed. Check if it's the one we want and adjust as
// needed.
[self setCategory:_category mode:_mode error:nil];
// needed. This notification is posted on a secondary thread, so make
// sure we switch to our worker thread.
dispatch_async(_workerQueue, ^{
[self setCategory:_category mode:_mode error:nil];
});
break;
}
default:
// Do nothing.
break;

View File

@@ -72,6 +72,11 @@ RCT_EXTERN void RCTRegisterModule(Class);
[JMCallKitProxy removeListener:self];
}
- (dispatch_queue_t)methodQueue {
// Make sure all our methods run in the main thread.
return dispatch_get_main_queue();
}
// End call
RCT_EXPORT_METHOD(endCall:(NSString *)callUUID
resolve:(RCTPromiseResolveBlock)resolve

View File

@@ -22,6 +22,7 @@ import Foundation
internal final class JMCallKitEmitter: NSObject, CXProviderDelegate {
private var listeners = Set<JMCallKitEventListenerWrapper>()
private var pendingMuteActions = Set<UUID>()
internal override init() {}
@@ -29,13 +30,10 @@ internal final class JMCallKitEmitter: NSObject, CXProviderDelegate {
func addListener(_ listener: JMCallKitListener) {
let wrapper = JMCallKitEventListenerWrapper(listener: listener)
objc_sync_enter(listeners)
listeners.insert(wrapper)
objc_sync_exit(listeners)
}
func removeListener(_ listener: JMCallKitListener) {
objc_sync_enter(listeners)
// XXX Constructing a new JMCallKitEventListenerWrapper instance in
// order to remove the specified listener from listeners is (1) a bit
// funny (though may make a statement about performance) and (2) not
@@ -58,76 +56,76 @@ internal final class JMCallKitEmitter: NSObject, CXProviderDelegate {
listeners.remove($0)
}
}
objc_sync_exit(listeners)
}
// MARK: - Add mute action
func addMuteAction(_ actionUUID: UUID) {
pendingMuteActions.insert(actionUUID)
}
// MARK: - CXProviderDelegate
func providerDidReset(_ provider: CXProvider) {
objc_sync_enter(listeners)
listeners.forEach { $0.listener?.providerDidReset?() }
objc_sync_exit(listeners)
pendingMuteActions.removeAll()
}
func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
objc_sync_enter(listeners)
listeners.forEach {
$0.listener?.performAnswerCall?(UUID: action.callUUID)
}
objc_sync_exit(listeners)
action.fulfill()
}
func provider(_ provider: CXProvider, perform action: CXEndCallAction) {
objc_sync_enter(listeners)
listeners.forEach {
$0.listener?.performEndCall?(UUID: action.callUUID)
}
objc_sync_exit(listeners)
action.fulfill()
}
func provider(_ provider: CXProvider, perform action: CXSetMutedCallAction) {
objc_sync_enter(listeners)
listeners.forEach {
$0.listener?.performSetMutedCall?(UUID: action.callUUID,
isMuted: action.isMuted)
let uuid = pendingMuteActions.remove(action.uuid)
// XXX avoid mute actions ping-pong: if the mute action was caused by
// the JS side (we requested a transaction) don't call the delegate
// method. If it was called by the provder itself (when the user presses
// the mute button in the CallKit view) then call the delegate method.
if (uuid == nil) {
listeners.forEach {
$0.listener?.performSetMutedCall?(UUID: action.callUUID,
isMuted: action.isMuted)
}
}
objc_sync_exit(listeners)
action.fulfill()
}
func provider(_ provider: CXProvider, perform action: CXStartCallAction) {
objc_sync_enter(listeners)
listeners.forEach {
$0.listener?.performStartCall?(UUID: action.callUUID,
isVideo: action.isVideo)
}
objc_sync_exit(listeners)
action.fulfill()
}
func provider(_ provider: CXProvider,
didActivate audioSession: AVAudioSession) {
objc_sync_enter(listeners)
listeners.forEach {
$0.listener?.providerDidActivateAudioSession?(session: audioSession)
}
objc_sync_exit(listeners)
}
func provider(_ provider: CXProvider,
didDeactivate audioSession: AVAudioSession) {
objc_sync_enter(listeners)
listeners.forEach {
$0.listener?.providerDidDeactivateAudioSession?(
session: audioSession)
}
objc_sync_exit(listeners)
}
}

View File

@@ -18,6 +18,8 @@ import CallKit
import Foundation
/// JitsiMeet CallKit proxy
// NOTE: The methods this class exposes are meant to be called in the UI thread.
// All delegate methods called by JMCallKitEmitter will be called in the UI thread.
@available(iOS 10.0, *)
@objc public final class JMCallKitProxy: NSObject {
@@ -151,6 +153,14 @@ import Foundation
completion: @escaping (Error?) -> Swift.Void) {
guard enabled else { return }
// XXX keep track of muted actions to avoid "ping-pong"ing. See
// JMCallKitEmitter for details on the CXSetMutedCallAction handling.
for action in transaction.actions {
if (action as? CXSetMutedCallAction) != nil {
emitter.addMuteAction(action.uuid)
}
}
callController.request(transaction, completion: completion)
}

View File

@@ -130,6 +130,7 @@ cp "${CERT_DIR}/dev-profile.mobileprovision" ~/Library/MobileDevice/Provisionin
npm install
cd ios
pod update
pod install
cd ..

View File

@@ -1,6 +1,5 @@
{
"contactlist": "__count__ Teilnehmer",
"contactlist_plural": "",
"contactlist_plural": "__count__ Teilnehmer",
"passwordSetRemotely": "von einem anderen Teilnehmer gesetzt",
"poweredby": "Betrieben von",
"inviteUrlDefaultMsg": "Die Konferenz wird erstellt...",
@@ -23,6 +22,7 @@
"react-nativeGrantPermissions": "Wählen Sie <b><i>Erlauben</i></b> wenn der Browser um Berechtigungen bittet.",
"chromeGrantPermissions": "Wählen Sie <b><i>Erlauben</i></b> wenn der Browser um Berechtigungen bittet.",
"androidGrantPermissions": "Wählen Sie <b><i>Erlauben</i></b> wenn der Browser um Berechtigungen bittet.",
"electronGrantPermissions": "Bitte Berechtigungen zur Verwendung der Kamera und des Mikrofons erteilen",
"firefoxGrantPermissions": "Wählen Sie <b><i>Markiertes Gerät teilen</i></b> wenn der Browser um Berechtigungen bittet.",
"operaGrantPermissions": "Wählen Sie <b><i>Erlauben</i></b> wenn der Browser um Berechtigungen bittet.",
"iexplorerGrantPermissions": "Wählen Sie <b><i>OK</i></b> wenn der Browser um Berechtigungen bittet.",
@@ -46,46 +46,20 @@
"showSpeakerStats": "Statistiken für Sprecher anzeigen"
},
"welcomepage": {
"disable": "Diesen Hinweis nicht mehr anzeigen",
"feature1": {
"content": "Kein Download nötig. __app__ läuft direkt im Browser. Einfach die Konferenzadresse teilen und los geht's.",
"title": "Einfach zu benutzen"
},
"feature2": {
"content": "Videokonferenzen mit mehreren Teilnehmen mit weniger als 128Kpbs. Bildschirmfreigaben und Telefonkonferenzen kommen sogar mit noch weniger Bandbreite aus.",
"title": "Niedrige Bandbreite"
},
"feature3": {
"content": "__app__ steht unter der Apache Lizenz. Es steht ihnen frei __app__ gemäss dieser Lizenz herunterzuladen, zu verändern oder zu verbreiten.",
"title": "Open Source"
},
"feature4": {
"content": "Es gibt keine künstlichen Begrenzungen der Anzahl der Konferenz-Teilnehmer. Die Bandbreite und Rechenleistung des Server sind die einzigen Limitierungen.",
"title": "Unbegrenzte Anzahl Benutzer"
},
"feature5": {
"content": "Es ist ganz einfach den Bildschirm zu teilen. __app__ ist ideal für Online-Präsentationen, Vorlesungen und Fernwartungsanfragen.",
"title": "Bildschirmfreigabe"
},
"feature6": {
"content": "Privatsphäre gewünscht? __app__ Konferenzen können mit einem Passwort geschützt werden um ungebetene Gäste fernzuhalten und Unterbrechungen zu vermeiden.",
"title": "Sichere Konferenzen"
},
"feature7": {
"content": "__app__ verwendet Etherpad, ein Editor zur kollaborativen Bearbeitung von Texten.",
"title": "Freigegebene Notizen"
},
"feature8": {
"content": "Die Verwendung kann durch die Integration mit Piwik, Google Analytics und anderen Überwachungs- und Statistikprogrammen protokolliert werden.",
"title": "Benutzungsstatistiken"
"appDescription": "Auf geht's! Beginne eine Videokonferenz mit dem ganzen Team. Oder eigentlich, lade alle ein die du kennst. __app__ ist eine vollständig verschlüsselte, aus 100% Open-Source-Software bestehende Videokonferenzlösung die du den ganzen Tag kostenlos verwenden kannst — ohne Registrierung.",
"audioVideoSwitch": {
"audio": "Sprache",
"video": "Video"
},
"calendar": "Kalender",
"go": "Los",
"join": "Beitreten",
"privacy": "Privatsphäre",
"roomname": "Konferenzname eingeben",
"roomnamePlaceHolder": "Konferenzname",
"roomnameHint": "Name oder URL der Konferenz der Sie beitreten möchten. Sie können einen Namen erfinden, er muss nur den anderen Teilnehmern übermittelt werden damit sie der gleichen Konferenz beitreten.",
"sendFeedback": "Senden Sie uns Ihr Feedback",
"terms": "Bedingungen"
"terms": "Bedingungen",
"title": "Sichere, flexible und vollständig freie Videokonferenzen"
},
"startupoverlay": {
"policyText": "",
@@ -99,22 +73,28 @@
"toolbar": {
"addPeople": "Teilnehmer zur Konferenz hinzufügen",
"audioonly": "Nur-Audio-Modus aktivieren/deaktivieren (spart Bandbreite)",
"callQuality": "Qualitätseinstellungen",
"enterFullScreen": "Vollbildmodus",
"exitFullScreen": "Vollbildmodus verlassen",
"feedback": "Feedback hinterlasen",
"moreActions": "Weitere Einstellungen",
"mute": "Stummschaltung aktivieren / deaktivieren",
"videomute": "Kamera starten / stoppen",
"authenticate": "Anmelden",
"lock": "Konferenz schützen / Schutz aufheben",
"invite": "Link teilen",
"chat": "Chat öffnen / schliessen",
"etherpad": "Geteiltes Dokument öffnen / schliessen",
"documentOpen": "Geteiltes Dokument öffnen",
"documentClose": "Geteiltes Dokument schliessen",
"sharedvideo": "YouTube-Video teilen",
"sharescreen": "Bildschirmfreigabe starten / stoppen",
"sharescreen": "Bildschirmfreigabe",
"stopSharedVideo": "YouTube Video stoppen",
"fullscreen": "Vollbildmodus aktivieren / deaktivieren",
"sip": "SIP Nummer anrufen",
"Settings": "Einstellungen",
"hangup": "Verlassen",
"login": "Anmelden",
"logout": "Abmelden",
"dialpad": "Wähltastatur öffnen / schliessen",
"sharedVideoMutedPopup": "Das freigegebene Video wurde stumm geschaltet um mit den anderen Teilnehmern zu sprechen.",
"micMutedPopup": "Das Mikrofon wurde stumm geschaltet um das freigegebene Video geniessen zu können.",
"talkWhileMutedPopup": "Versuchen sie zu sprechen? Ihr Mikrofon ist stummgeschaltet.",
@@ -123,17 +103,9 @@
"micDisabled": "Kein Mikrofon verfügbar",
"filmstrip": "Videos anzeigen / verbergen",
"profile": "Profil bearbeiten",
"raiseHand": "Hand erheben"
},
"unsupportedBrowser": {
"appNotInstalled": "Mit __app__ auf dem Mobiltelefon teilnehmen.",
"downloadApp": "App herunterladen",
"openApp": "In __app__ fortfahren"
},
"bottomtoolbar": {
"chat": "Chat öffnen / schliessen",
"filmstrip": "Videos anzeigen / verbergen",
"contactlist": "Teilnehmerliste und neue Teilnehmer einladen"
"raiseHand": "Hand erheben",
"shortcuts": "Tastenkürzel anzeigen",
"speakerStats": "Sprecher-Statistiken"
},
"chat": {
"nickname": {
@@ -218,10 +190,11 @@
"grantedToUnknown": "Moderatorenrechte an $t(notify.somebody) vergeben.",
"muted": "Der Konferenz wurde stumm beigetreten.",
"mutedTitle": "Stummschaltung aktiv!",
"raisedHand": "Möchte sprechen."
"raisedHand": "Möchte sprechen.",
"suboptimalExperienceTitle": "Browserwarnung",
"suboptimalExperienceDescription": "Tut uns leid, aber die Konferenz wird mit __appName__ kein grossartiges Erlebnis. Wir versuchen immer die Situation zu verbessern, bis dahin empfehlen wir aber die Verwendung einer der <a href=\"static/recommendedBrowsers.html\" target=\"_blank\">vollständig unterstützen Browser</a>."
},
"dialog": {
"add": "Hinzufügen",
"allow": "Erlauben",
"kickMessage": "Oh! Sie wurden aus der Konferenz ausgeschlossen.",
"popupErrorTitle": "Popup blockiert",
@@ -236,7 +209,6 @@
"copy": "Kopieren",
"contactSupport": "Support kontaktieren",
"error": "Fehler",
"createPassword": "Passwort erstellen",
"detectext": "Fehler bei der Erkennung der Bildschirmfreigabeerweiterung.",
"failedpermissions": "Die Zugriffsberechtigungen auf das Mikrofon und/oder die Kamera konnten nicht eingeholt werden.",
"conferenceReloadTitle": "Leider ist etwas schiefgegangen.",
@@ -287,10 +259,6 @@
"Save": "Speichern",
"recording": "Aufnahme",
"recordingToken": "Aufnahme-Token eingeben",
"passwordCheck": "Sind Sie sicher, dass Sie das Passwort entfernen möchten?",
"passwordMsg": "Passwort setzen um die Konferenz zu schützen",
"shareLink": "Link zu dieser Konferenz teilen",
"yourPassword": "Neues Passwort eingeben",
"Back": "Zurück",
"serviceUnavailable": "Dienst nicht verfügbar",
"gracefulShutdown": "Der Dienst steht momentan wegen Wartungsarbeiten nicht zur Verfügung. Bitte versuchen Sie es später noch einmal.",
@@ -304,15 +272,14 @@
"tokenAuthFailed": "Sie sind nicht berechtigt dieser Konferenz beizutreten.",
"displayNameRequired": "Anzeigename ist erforderlich",
"enterDisplayName": "Geben Sie Ihren Anzeigenamen ein",
"extensionRequired": "Erweiterung erforderlich:",
"firefoxExtensionPrompt": "Um die Bildschirmfreigabe nutzen zu können, muss eine Firefox-Erweiterung installiert werden. Bitte versuchen Sie es erneut nachdem die <a href='__url__'>Erweiterung installiert</a> wurde.",
"feedbackHelp": "Ihr Feedback hilft uns die Qualität der Konferenzen zu verbessern.",
"feedbackQuestion": "Anmerkungen zur Konferenz.",
"thankYou": "Danke für die Verwendung von __appName__!",
"sorryFeedback": "Tut uns leid. Möchten Sie uns mehr mitteilen?",
"liveStreaming": "Live-Streaming",
"streamKey": "Streamname/-schlüssel",
"streamKey": "Name/Schlüssel für den Stream",
"startLiveStreaming": "Live-Streaming starten",
"startRecording": "Aufnahme starten",
"stopStreamingWarning": "Sind Sie sicher dass Sie das Live-Streaming stoppen möchten?",
"stopRecordingWarning": "Sind Sie sicher dass Sie die Aufnahme stoppen möchten?",
"stopLiveStreaming": "Live-Streaming stoppen",
@@ -321,6 +288,8 @@
"permissionDenied": "Zugriff verweigert",
"screenSharingFailedToInstall": "Oh! Die Erweiterung für die Bildschirmfreigabe konnte nicht installiert werden.",
"screenSharingFailedToInstallTitle": "Bildschirmfreigabe-Erweiterung konnte nicht installiert werden",
"screenSharingFirefoxPermissionDeniedError": "Die Bildschirmfreigabe ist leider fehlgeschlagen. Bitte stellen Sie sicher, dass die Berechtigung für die Bildschirmfreigabe im Browser erteilt wurde.",
"screenSharingFirefoxPermissionDeniedTitle": "Die Bildschirmfreigabe konnte nicht gestartet werden.",
"screenSharingPermissionDeniedError": "Oh! Beim Anfordern der Bildschirmfreigabe-Berechtigungen hat etwas nicht funktioniert. Bitte aktualisieren und erneut versuchen.",
"cameraUnsupportedResolutionError": "Die Kamera unterstützt die erforderliche Auflösung nicht.",
"cameraUnknownError": "Die Kamera kann aus einem unbekannten Grund nicht verwendet werden.",
@@ -417,14 +386,21 @@
"busy": "Es werden Resourcen zum Streamen bereitgestellt. Bitte in ein paar Minuten erneut versuchen.",
"busyTitle": "Alle Streaming-Instanzen sind in Gebrauch",
"buttonTooltip": "Live-Stream starten / stoppen",
"changeSignIn": "Konten wechseln.",
"choose": "Live stream auswählen",
"chooseCTA": "Streaming-Option auswählen. Sie sind aktuell als __email__ angemeldet.",
"enterStreamKey": "Name/Schlüssel für den YouTube Livestream hier eingeben.",
"error": "Das Live-Streaming ist fehlgeschlagen. Bitte versuchen Sie es erneut.",
"errorAPI": "Beim abrufen der YouTube Livestreams ist ein Fehler aufgetreten. Bitte versuchen Sie sich erneut anzumelden.",
"failedToStart": "Live-Streaming konnte nicht gestartet werden",
"off": "Live-Streaming gestoppt",
"on": "Live-Streaming",
"pending": "Live-Stream wird gestartet...",
"serviceName": "Live Streaming-Dienst",
"streamIdRequired": "Bitte Stream-ID eingeben um das Live-Streaming zu starten.",
"streamIdHelp": "Wo ist die Stream-ID zu finden?",
"signIn": "Mit Google anmelden",
"signInCTA": "Anmelden oder den Name/Schlüssel des YouTube Livestreams eingeben.",
"start": "Einen Livestream starten",
"streamIdHelp": "Was ist das?",
"unavailableTitle": "Live-Streaming nicht verfügbar"
},
"videoSIPGW": {
@@ -454,26 +430,16 @@
"selectADevice": "Ein Gerät wählen",
"testAudio": "Audio testen"
},
"invite": {
"addPassword": "Passwort hinzufügen",
"callNumber": "__number__ anrufen",
"enterID": "Um mit einem Telefon teilzunehmen, geben Sie die Konferenz ID (__conferenceID__) gefolgt von # ein",
"howToDialIn": "Wählen Sie eine der folgenden Nummern um via Telefon teilzunehmen und die Konferenz ID",
"hidePassword": "Passwort verstecken",
"inviteTo": "Teilnehmer zu __conferenceName__ einladen",
"invitedYouTo": "Sie wurden von __userName__ zur Konferenz __inviteURL__ eingeladen",
"invitePeople": "Einladen",
"locked": "Diese Konferenz ist gesperrt. Neue Teilnehmer müssen das Passwort eingeben um beizutreten.",
"showPassword": "Passwort anzeigen",
"unlocked": "Die Konferenz ist nicht geschützt. Jeder mit dem Link kann der Konferenz beitreten."
},
"videoStatus": {
"callQuality": "Konferenzqualität",
"hd": "HD",
"hdTooltip": "Video wird in HD angezeigt",
"highDefinition": "Hohe Auflösung",
"labelTooltipVideo": "Aktuelle Videoqualität",
"labelTooltipAudioOnly": "Nur-Audio Modus aktiv",
"labelTooiltipNoVideo": "Kein Video",
"labelTooltipVideo": "Aktuelle Videoqualität",
"ld": "LD",
"ldTooltip": "Video wird in niedriger Auflösung angezeigt",
"lowDefinition": "Niedrige Auflösung",
"onlyAudioAvailable": "Nur Ton",
"onlyAudioSupported": "In diesem Browser wird nur Audio unterstützt.",
@@ -481,21 +447,30 @@
"p2pVideoQualityDescription": "Im Ende-zu-Ende Modus kann die Konferenzqualität nur zwischen hoch und nur-Audio gewählt werden. Andere Einstellungen werden nicht beachtet.",
"recHighDefinitionOnly": "Hohe Qualität wird bevorzugt.",
"sd": "SD",
"sdTooltip": "Video wird in Standardauflösung angezeigt",
"standardDefinition": "Standardauflösung",
"qualityButtonTip": "Empfangene Videoqualität ändern"
},
"dialOut": {
"dial": "Wählen",
"dialOut": "Nummer anrufen",
"statusMessage": "ist jetzt __status__",
"enterPhone": "Telefonnummer eingeben",
"phoneNotAllowed": "Diese Telefonnummer wird leider noch nicht unterstützt!"
"statusMessage": "ist jetzt __status__"
},
"addPeople": {
"add": "Hinzufügen",
"add": "Einladen",
"countryNotSupported": "Wir unterstützen dieses Land noch nicht.",
"countryReminder": "Telefonnummer nicht in den USA? Bitte sicherstellen, dass die Telefonnummer mit dem Ländercode beginnt.",
"disabled": "",
"invite": "Einladen",
"loading": "Suche nach Teilnehmern und Telefonnummern",
"loadingNumber": "Telefonnummer wird überprüft",
"loadingPeople": "Suche nach einzuladenden Teilnehmern",
"noResults": "Keine passenden Ergebnisse",
"searchPlaceholder": "Nach Teilnehmern und Konferenzen suchen",
"title": "Teilnehmer zur Konferenz hinzufügen",
"noValidNumbers": "Telefonnummer eingeben",
"notAvailable": "Sie können keine Teilnehmer einladen.",
"searchNumbers": "Telefonnummern hinzufügen",
"searchPeople": "Nach Teilnehmern suchen",
"searchPeopleAndNumbers": "Nach Teilnehmen suchen oder deren Telefonnummern hinzufügen",
"telephone": "Telefon: __number__",
"title": "Teilnehmer zu dieser Konferenz einladen",
"failedToAdd": "Teilnehmer konnte nicht hinzugefügt werden"
},
"inlineDialogFailure": {
@@ -514,14 +489,71 @@
"average": "Durschnittlich",
"bad": "Schlecht",
"good": "Gut",
"rateExperience": "Bitte bewerten Sie diese Konferenz.",
"detailsLabel": "Sagen Sie uns mehr dazu.",
"rateExperience": "Bitte bewerten Sie diese Konferenz",
"veryBad": "Sehr schlecht",
"veryGood": "Sehr gut"
},
"info": {
"copy": "Link kopieren",
"invite": "In __app__ einladen",
"title": "Konferenz-Zugriffsinformationen",
"addPassword": "Passwort hinzufügen",
"cancelPassword": "Password abbrechen",
"conferenceURL": "Link:",
"country": "Land",
"dialANumber": "Um dieser Konferenz beizutreten, rufen Sie eine dieser Telefonnummern an und geben die PIN __conferenceID__# ein.",
"dialInNumber": "Einwählen:",
"dialInConferenceID": "PIN:",
"dialInNotSupported": "Tut uns leid, einwählen ist momentan nicht unterstützt.",
"genericError": "Es ist leider etwas schiefgegangen.",
"inviteLiveStream": "Klicken Sie auf __url__ um den Livestream dieser Konferenz zu öffnen",
"invitePhone": "Um der Konferenz telefonisch beizutreten, rufen Sie __number__ and und geben die PIN __conferenceID__# ein",
"invitePhoneAlternatives": "Klicken sie auf __url__ um mehr Telefonnummern anzuzeigen",
"inviteURL": "Klicken Sie auf __url__ um der Konferenz beizutreten",
"liveStreamURL": "Livestream:",
"moreNumbers": "Weitere Telefonnummern",
"noNumbers": "Keine Telefonnummern verfügbar.",
"noPassword": "Kein",
"noRoom": "Keine Konferenz für die Einwähl-Informationen angegeben.",
"numbers": "Einwählnummern",
"password": "Passwort:",
"title": "Teilen",
"tooltip": "Zugriffsinformationen über die Konferenz abrufen"
},
"settingsView": {
"alertOk": "OK",
"alertTitle": "Warnung",
"alertURLText": "Die angegebene Server URL ist ungültig",
"conferenceSection": "Konferenz",
"displayName": "Anzeigename",
"email": "E-Mail",
"header": "Einstellungen",
"profileSection": "Profil",
"serverURL": "Server URL",
"startWithAudioMuted": "Stumm beitreten",
"startWithVideoMuted": "Ohne Video beitreten"
},
"calendarSync": {
"later": "Später",
"next": "Folgend",
"nextMeeting": "Nächste Konferenz",
"now": "Jetzt",
"permissionButton": "Einstellungen öffnen",
"permissionMessage": "Die App benötigt Zugriff auf den Kalender um die Termine und Konferenzen anzuzeigen."
},
"recentList": {
"today": "Heute",
"yesterday": "Gestern",
"earlier": "Früher"
},
"sectionList": {
"pullToRefresh": "Ziehen um zu aktualisieren"
},
"deepLinking": {
"title": "Die Konferenz wird in __app__ geöffnet...",
"description": "Nichts passiert? Wir haben versucht die Konferenz in __app__ zu öffnen. Versuchen Sie es erneut oder treten Sie der Konferenz in __app__ im Web bei.",
"tryAgainButton": "Erneut mit der nativen Applikation versuchen",
"launchWebButton": "Im Web öffnen",
"appNotInstalled": "Sie benötigen die __app__ App um der Konferenz auf dem Smartphone beizutreten.",
"downloadApp": "App herunterladen",
"openApp": "In der App fortfahren"
}
}

View File

@@ -1,24 +1,19 @@
{
"contactlist": "",
"contactlist_plural": "",
"passwordSetRemotely": "Configurado por outro membro",
"connectionsettings": "Configurações de conexão",
"contactlist_plural": "__count__ Membros",
"passwordSetRemotely": "configurado por outro membro",
"poweredby": "distribuído por",
"feedback": {
"average": "Média",
"bad": "Ruim",
"good": "Boa",
"rateExperience": "Por favor, avalie sua experiência na reunião.",
"veryBad": "Muito ruim",
"veryGood": "Muito boa"
},
"inviteUrlDefaultMsg": "Sua conferência está sendo criada...",
"me": "eu",
"speaker": "Orador",
"raisedHand": "Gostaria de falar",
"defaultNickname": "ex. João Pedro",
"defaultLink": "ex.: __url__",
"callingName": "__name__",
"audioDevices": {
"bluetooth": "Bluetooth",
"headphones": "Fones de ouvido",
"phone": "Celular",
"speaker": "Orador"
},
"audioOnly": {
"audioOnly": "Somente áudio",
"featureToggleDisabled": "A alternância de __feature__ é desativada enquanto estiver no modo somente de áudio"
@@ -27,6 +22,7 @@
"react-nativeGrantPermissions": "Selecione <b><i>Permitir</i></b> quando seu navegador perguntar pelas permissões.",
"chromeGrantPermissions": "Selecione <b><i>Permitir</i></b> quando seu navegador perguntar pelas permissões.",
"androidGrantPermissions": "Selecione <b><i>Permitir</i></b> quando seu navegador perguntar pelas permissões.",
"electronGrantPermissions": "Dê as permissões para usar sua câmera e microfone",
"firefoxGrantPermissions": "Selecione <b><i>Compartilhar Dispositivos Selecionados</i></b> quando seu navegador perguntar pelas permissões.",
"operaGrantPermissions": "Selecione <b><i>Permitir</i></b> quando seu navegador perguntar pelas permissões.",
"iexplorerGrantPermissions": "Selecione <b><i>OK</i></b> quando seu navegador perguntar pelas permissões.",
@@ -50,46 +46,20 @@
"showSpeakerStats": "Exibir estatísticas do alto falante"
},
"welcomepage": {
"disable": "Não exibir esta página novamente",
"feature1": {
"content": "Não precisa baixar nada. __app__ funciona diretamente no seu navegador. Basta compartilhar a URL da sua conferência com outros para começar.",
"title": "Simples de usar"
},
"feature2": {
"content": "Conferências de vídeo de multipartes funcionam a partir de 128 kbps. Compartilhamento de tela e conferências apenas com áudio são possíveis com muito menos.",
"title": "Largura de banda baixa"
},
"feature3": {
"content": "__app__ é licenciado sob a Licença Apache. Você é livre para baixar, usar, modificar e compartilhar ela com a mesma licença.",
"title": "Código aberto"
},
"feature4": {
"content": "Não há restrições artificiais sobre o número de usuários ou membros da conferência. O poder do servidor e a largura de banda são os únicos fatores limitantes.",
"title": "Usuários ilimitados"
},
"feature5": {
"content": "É fácil compartilhar sua tela com outros. __app__ é ideal para apresentações online, leituras, e sessões de suporte técnico.",
"title": "Compartilhamento de tela"
},
"feature6": {
"content": "Precisa alguma privacidade? Salas de conferência do __app__ podem ser seguras com uma senha para excluir visitantes indesejados e prevenir interrupções.",
"title": "Salas seguras"
},
"feature7": {
"content": "__app_ disponibiliza o Etherpad, um editor de texto colaborativo em tempo real, que é ótimo para reuniões rápidas, escrevendo artigos, e mais.",
"title": "Notas compartilhadas"
},
"feature8": {
"content": "Aprenda sobre seus usuários através de integração fácil com o Piwik, Google Analytics, e outros sistemas de monitoramento e estatísticas.",
"title": "Estatísticas de uso"
"appDescription": "Vá em frente, converse por vídeo com toda a equipe. De fato, convide todos que você conhece. __app__ é uma solução de videoconferência totalmente criptografada e 100% de código aberto que você pode usar todos os dias, a cada dia, gratuitamente — sem necessidade de conta.",
"audioVideoSwitch": {
"audio": "Voz",
"video": "Vídeo"
},
"calendar": "Calendário",
"go": "IR",
"join": "Entrar",
"privacy": "Política de Privacidade",
"roomname": "Digite o nome da sala",
"roomnamePlaceHolder": "Nome da sala",
"roomnameHint": "Digite o nome ou a URL da sala que você deseja entrar. Você pode digitar um nome, e apenas deixe para as pessoas que você quer se reunir digitem o mesmo nome.",
"sendFeedback": "Enviar comentários",
"terms": "Termos"
"terms": "Termos",
"title": "Vídeo conferência mais segura, mais flexível e completamente livre "
},
"startupoverlay": {
"policyText": " ",
@@ -103,22 +73,28 @@
"toolbar": {
"addPeople": "Adicionar pessoas à sua chamada",
"audioonly": "Ativar / desativar modo somente áudio (economiza banda)",
"callQuality": "Gerenciar qualidade da chamada",
"enterFullScreen": "Ver em tela cheia",
"exitFullScreen": "Sair da tela cheia",
"feedback": "Deixar feedback",
"moreActions": "Mais ações",
"mute": "Mudo / Não mudo",
"videomute": "Iniciar ou parar a câmera",
"authenticate": "Autenticar",
"lock": "Travar ou destravar a sala",
"invite": "Compartilhar o link",
"chat": "Abrir ou fechar o bate-papo",
"etherpad": "Abrir ou fechar o documento compartilhado",
"documentOpen": "Abrir documento compartilhado",
"documentClose": "Fechar documento compartilhado",
"sharedvideo": "Compartilhar um vídeo do YouTube",
"sharescreen": "Iniciar ou parar o compartilhamento de tela",
"sharescreen": "Compartilhamento de tela",
"stopSharedVideo": "Parar vídeo do YouTube",
"fullscreen": "Entrar ou sair da tela cheia",
"sip": "Chamar número SIP",
"Settings": "Configurações",
"hangup": "Sair",
"login": "Iniciar sessão",
"logout": "Encerrar sessão",
"dialpad": "Abrir ou fechar teclado de discagem",
"sharedVideoMutedPopup": "Seu vídeo compartilhado foi silenciado para que você possa conversar com os outros membros.",
"micMutedPopup": "Seu microfone foi silenciado para que você aproveite plenamente seu vídeo compartilhado.",
"talkWhileMutedPopup": "Tentando falar? Você está em mudo.",
@@ -127,19 +103,9 @@
"micDisabled": "O microfone não está disponível",
"filmstrip": "Mostrar / ocultar vídeos",
"profile": "Editar seu perfil",
"raiseHand": "Erguer o baixar sua mão"
},
"unsupportedBrowser": {
"appInstalled": "ou se você já tenha isso<br /> <strong>então</strong>",
"appNotInstalled": "Você precisa do <strong>__app__</strong> para começar uma conversa no seu celular",
"downloadApp": "Baixe o Aplicativo",
"joinConversation": "Entrar na conversa",
"startConference": "Comece uma conferência"
},
"bottomtoolbar": {
"chat": "Abrir / fechar bate-papo",
"filmstrip": "Mostrar / ocultar vídeos",
"contactlist": "Veja e convide membros"
"raiseHand": "Erguer o baixar sua mão",
"shortcuts": "Ver atalhos",
"speakerStats": "Estatísticas do Apresentador"
},
"chat": {
"nickname": {
@@ -174,7 +140,7 @@
"moderator": "Moderador",
"videomute": "O membro parou a câmera",
"mute": "O membro está em silêncio",
"kick": "Chutar fora",
"kick": "Expulsar",
"muted": "Mudo",
"domute": "Mudo",
"flip": "Inverter",
@@ -199,7 +165,7 @@
"remoteaddress_plural": "Endereços remotos:",
"transport": "Transporte:",
"bandwidth": "Largura de banda estimada:",
"na": "Volte aqui para informações de conexão uma vez que a conferência inicie",
"na": "Volte aqui para informações de conexão após iniciar a conferência",
"turn": " (virar)",
"quality": {
"good": "Boa",
@@ -213,7 +179,9 @@
"notify": {
"disconnected": "desconectado",
"moderator": "Direitos de moderador concedidos!",
"connected": "conectado",
"connectedOneMember": "__name__ conectado",
"connectedTwoMembers": "__first__ e __second__ conectados",
"connectedThreePlusMembers": "__name__ e __count__ outros conectados",
"somebody": "Alguém",
"me": "Eu",
"focus": "Foco da conferência",
@@ -222,42 +190,42 @@
"grantedToUnknown": "Direitos de moderador concedido para $t(notify.somebody)!",
"muted": "Você iniciou uma conversa em mudo.",
"mutedTitle": "Você está mudo!",
"raisedHand": "Gostaria de falar."
"raisedHand": "Gostaria de falar.",
"suboptimalExperienceTitle": "Alerta do navegador",
"suboptimalExperienceDescription": "Eer ... temos medo de que sua experiência com o __appName__ não seja tão boa aqui. Estamos procurando maneiras de melhorar isso, mas até lá tente usar um dos <a href='static/recommendedBrowsers.html' target='_blank'> navegadores totalmente compatíveis</a>."
},
"dialog": {
"add": "Adicionar",
"allow": "Permitir",
"kickMessage": "Ouch! Você o chutou para fora da reunião!",
"popupErrorTitle": "",
"popupError": "",
"kickMessage": "Ouch! Você foi expulso da reunião!",
"popupErrorTitle": "Popup bloqueado",
"popupError": "Seu navegador está bloqueando janelas popup deste site. Habilite os popups nas configurações de segurança no seu navegador e tente novamente.",
"passwordErrorTitle": "Erro na senha",
"passwordError": "Esta conversa está protegida atualmente por uma senha. Somente o dono da conferência pode definir a senha.",
"passwordError2": "Esta conversa não está protegida por senha atualmente. Somente o dono da conferência pode definir a senha.",
"connectError": "Oops! Alguma coisa está errada e nós não pudemos conectar à conferência.",
"connectErrorWithMsg": "Oops! Alguma coisa está errada e não podemos conectar à conferência: __msg__",
"incorrectPassword": "",
"incorrectPassword": "Usuário ou senha incorretos",
"connecting": "Conectando",
"copy": "Copiar",
"contactSupport": "",
"contactSupport": "Contate o suporte",
"error": "Erro",
"createPassword": "Criar uma senha",
"detectext": "Erro enquanto tenta detectar a extensão de compartilhamento de tela.",
"detectext": "Erro ao detectar a extensão de compartilhamento de tela.",
"failedpermissions": "Falha ao obter permissões para usar o microfone e/ou câmera local.",
"conferenceReloadTitle": "Infelizmente, algo deu errado.",
"conferenceReloadMsg": "Estamos tentando consertar isto. Reconectando em __seconds__ segundos...",
"conferenceDisconnectTitle": "Você foi desconectado.",
"conferenceDisconnectMsg": "Você pode querer verificar sua conexão de rede. Reconectando em __seconds__ segundos ...",
"dismiss": "",
"rejoinNow": "Voltar agora",
"maxUsersLimitReachedTitle": "",
"maxUsersLimitReached": "",
"dismiss": "Dispensar",
"rejoinNow": "Reconectar agora",
"maxUsersLimitReachedTitle": "Limite máximo de membros alcançado",
"maxUsersLimitReached": "O limite para o máximo do número de membros foi alcançado. A conferência está cheia. Contate o dono da reunião ou tente de novo depois!",
"lockTitle": "Bloqueio falhou",
"lockMessage": "Falha ao travar a conferência.",
"warning": "Atenção",
"passwordNotSupportedTitle": "",
"passwordNotSupported": "",
"passwordNotSupportedTitle": "Senha não suportada",
"passwordNotSupported": "Configuração de senha para a reunião não é suportada.",
"internalErrorTitle": "Erro interno",
"internalError": "",
"internalError": "Oops! Alguma coisa está errada. O seguinte erro ocorreu: __error__",
"unableToSwitch": "Impossível trocar o fluxo de vídeo.",
"SLDFailure": "Oops! Alguma coisa está errada e nós falhamos em silenciar! (Falha do SLD)",
"SRDFailure": "Oops! Alguma coisa está errada e nós falhamos em parar o vídeo! (Falha do SRD)",
@@ -274,8 +242,8 @@
"shareVideoLinkError": "Por favor, forneça um link do youtube correto.",
"removeSharedVideoTitle": "Remover vídeo compartilhado",
"removeSharedVideoMsg": "Deseja remover seu vídeo compartilhado?",
"alreadySharedVideoMsg": "",
"alreadySharedVideoTitle": "",
"alreadySharedVideoMsg": "Outro membro já está compartilhando um vídeo. Esta conferência permite apenas um vídeo compartilhado por vez.",
"alreadySharedVideoTitle": "Somente um vídeo compartilhado é permitido por vez",
"WaitingForHost": "Esperando o hospedeiro...",
"WaitForHostMsg": "A conferência <b>__room__</b> não foi iniciada. Se você é o hospedeiro, então autentique-se. Caso contrário, aguarde o hospedeiro chegar.",
"IamHost": "Eu sou o hospedeiro",
@@ -284,20 +252,16 @@
"retry": "Tentar novamente",
"logoutTitle": "Encerrar sessão",
"logoutQuestion": "Deseja encerrar a sessão e finalizar a conferência?",
"sessTerminated": "",
"sessTerminated": "Chamada terminada",
"hungUp": "Você desconectou",
"joinAgain": "Entrar novamente",
"Share": "Compartilhar",
"Save": "Salvar",
"recording": "Gravando",
"recordingToken": "Digite o token de gravação",
"passwordCheck": "Deseja remover a senha?",
"passwordMsg": "Definir uma senha para trancar sua sala",
"shareLink": "Compartilhar o link para a chamada",
"yourPassword": "Digite a nova senha",
"Back": "Voltar",
"serviceUnavailable": "Serviço indisponível",
"gracefulShutdown": "Nosso serviço está desligado para manutenção. Por favor, tente mais tarde.",
"gracefulShutdown": "O sistema está em manutenção. Por favor tente novamente mais tarde.",
"Yes": "Sim",
"reservationError": "Erro de sistema de reserva",
"reservationErrorMsg": "Código do erro: __code__, mensagem: __msg__",
@@ -306,42 +270,40 @@
"token": "token",
"tokenAuthFailedTitle": "Falha de autenticação",
"tokenAuthFailed": "Desculpe, você não está autorizado a entrar nesta chamada.",
"displayNameRequired": "Mostrar o nome é requerido",
"displayNameRequired": "Nome de exibição requerido",
"enterDisplayName": "Digite seu nome de exibição",
"extensionRequired": "Extensão requerida:",
"firefoxExtensionPrompt": "Você precisa instalar uma extensão do Firefox para compartilhar a tela. Tente novamente depois que você <a href='__url__'>pegá-lo aqui</a>!",
"feedbackHelp": "Seu retorno nos ajudará a melhorar nossa experiência de vídeo.",
"feedbackQuestion": "Nos conte sobre sua chamada!",
"thankYou": "Obrigado por usar o __appName__!",
"sorryFeedback": "Lamentamos escutar isso. Gostaria de nos contar mais?",
"sorryFeedback": "Sentimos muito pelos transtornos. Poderia nos das mais detalhes?",
"liveStreaming": "Transmissão ao Vivo",
"streamKey": "Nome/chave do fluxo",
"startLiveStreaming": "Iniciar transmissão ao vivo",
"streamKey": "Chave para transmissão ao vivo",
"startLiveStreaming": "Ir ao vivo agora",
"startRecording": "Parar gravação",
"stopStreamingWarning": "Tem certeza que deseja parar a transmissão ao vivo?",
"stopRecordingWarning": "Tem certeza que deseja parar a gravação?",
"stopLiveStreaming": "Parar a transmissão ao vivo",
"stopRecording": "Parar a gravação",
"doNotShowWarningAgain": "Não exibir este aviso novamente",
"doNotShowMessageAgain": "Não mostre esta mensagem novamente",
"permissionDenied": "Permissão Negada",
"screenSharingFailedToInstall": "",
"screenSharingFailedToInstallTitle": "",
"screenSharingPermissionDeniedError": "",
"micErrorPresent": "Ocorreu um erro conectando seu microfone.",
"cameraErrorPresent": "Ocorreu um erro conectando sua câmera.",
"screenSharingFailedToInstall": "Oops! Falhou a instalação da extensão de compartilhamento de tela.",
"screenSharingFailedToInstallTitle": "A extensão de compartilhamento de tela falhou ao instalar",
"screenSharingFirefoxPermissionDeniedError": "Algo deu errado enquanto estávamos tentando compartilhar sua tela. Por favor, certifique-se de que você nos deu permissão para fazê-lo. ",
"screenSharingFirefoxPermissionDeniedTitle": "Opa! Não foi possível iniciar o compartilhamento de tela.",
"screenSharingPermissionDeniedError": "Oops! Alguma coisa está errada com suas permissões de compartilhamento de tela. Recarregue e tente de novo.",
"cameraUnsupportedResolutionError": "Sua câmera não suporta a resolução de vídeo requerida.",
"cameraUnknownError": "Não pode usar a câmera por uma razão desconhecida.",
"cameraPermissionDeniedError": "Você não tem permissão para usar sua câmera. Você ainda pode entrar na conferência, mas os outros não verão você. Use o botão da câmera na barra de endereço para fixar isto.",
"cameraPermissionDeniedError": "Não foi permitido acessar a sua câmera. Você ainda pode entrar na conferência, mas sem exibir o seu vídeo. Clique no botão da câmera para tentar reparar.",
"cameraNotFoundError": "A câmera não foi encontrada.",
"cameraConstraintFailedError": "Sua câmera não satisfaz algumas condições necessárias.",
"micUnknownError": "Não pode usar o microfone por uma razão desconhecida.",
"micPermissionDeniedError": "Você não tem permissão para usar seu microfone. Você ainda pode entrar na conferência, mas os outros não ouvirão você. Use o botão da câmera na barra de endereço para fixar isto.",
"micPermissionDeniedError": "Não foi permitido acessar o seu microfone. Você ainda pode entrar na conferência, mas sem enviar áudio. Clique no botão do microfone para tentar reparar.",
"micNotFoundError": "O microfone não foi encontrado.",
"micConstraintFailedError": "Seu microfone não satisfaz algumas condições necessárias.",
"micNotSendingDataTitle": "",
"micNotSendingData": "",
"cameraNotSendingDataTitle": "",
"cameraNotSendingData": "",
"micNotSendingDataTitle": "Incapaz de acessar o microfone",
"micNotSendingData": "Estamos incapazes de acessar seu microfone. Selecione outro dispositivo do menu de configurações ou tente recarregar a aplicação.",
"cameraNotSendingDataTitle": "Incapaz de acessar a câmera",
"cameraNotSendingData": "Estamos incapazes de acessar sua câmera. Verifique se outra aplicação está usando este dispositivo, selecione outro dispositivo do menu de configurações ou recarregue a aplicação.",
"goToStore": "Vá para a loja virtual",
"externalInstallationTitle": "Extensão requerida",
"externalInstallationMsg": "Você precisa instalar nossa extensão de compartilhamento de tela.",
@@ -351,7 +313,7 @@
"muteParticipantBody": "Você não está habilitado para tirar o mudo deles, mas eles podem tirar o mudo deles mesmos a qualquer tempo.",
"muteParticipantButton": "Mudo",
"remoteControlTitle": "Conexão de área de trabalho remota",
"remoteControlRequestMessage": "Permitirá __user__ controlar remotamente sua área de trabalho?",
"remoteControlRequestMessage": "Deseja permitir que __user__ controle remotamente sua área de trabalho?",
"remoteControlShareScreenWarning": "Note que se você pressionar \"Permitir\" você vai compartilhar sua tela!",
"remoteControlDeniedMessage": "__user__ rejeitou sua requisição de controle remoto!",
"remoteControlAllowedMessage": "__user__ aceitou sua requisição de controle remoto!",
@@ -404,30 +366,50 @@
"ATTACHED": "Anexado"
},
"recording": {
"busy": "",
"busyTitle": "",
"busy": "Estamos trabalhando para liberar recursos de gravação. Tente novamente em alguns minutos.",
"busyTitle": "Todas as gravações estão atualmente ocupadas",
"buttonTooltip": "Iniciar / parar gravação",
"error": "A gravação falhou. Tente novamente.",
"failedToStart": "Falha ao iniciar a gravação",
"off": "Gravação parada",
"on": "Gravando",
"pending": "Aguardando um participante para iniciar a gravação...",
"unavailable": "",
"unavailableTitle": ""
"serviceName": "Serviço de gravação",
"unavailable": "Oops! O __serviceName__ está indisponível. Estamos trabalhando para resolver o problema. Por favor, tente mais tarde.",
"unavailableTitle": "Gravação indisponível"
},
"liveStreaming": {
"busy": "",
"busyTitle": "",
"buttonTooltip": "",
"error": "",
"failedToStart": "",
"off": "",
"busy": "Estamos trabalhando para liberar os recursos de transmissão. Tente novamente em alguns minutos.",
"busyTitle": "Todas as transmissões estão atualmente ocupadas",
"buttonTooltip": "Iniciar / Parar transmissão ao vivo",
"changeSignIn": "Alternar contas.",
"choose": "Escolha uma transmissão ao vivo",
"chooseCTA": "Escolha uma opção de transmissão. Você está conectado atualmente como __email__.",
"enterStreamKey": "Insira sua chave de transmissão ao vivo do YouTube aqui.",
"error": "Falha na transmissão ao vivo. Tente de novo.",
"errorAPI": "Ocorreu um erro ao acessar suas transmissões do YouTube. Por favor tente logar novamente.",
"failedToStart": "Falha ao iniciar a transmissão ao vivo",
"off": "Transmissão ao vivo encerrada",
"on": "Transmissão ao Vivo",
"pending": "Iniciando Transmissão ao Vivo...",
"streamIdRequired": "",
"streamIdHelp": "Aonde eu encontro isto?",
"unavailable": "",
"unavailableTitle": ""
"serviceName": "Serviço de Transmissão ao Vivo",
"signIn": "Faça login no Google",
"signInCTA": "Faça login ou insira sua chave de transmissão ao vivo do YouTube.",
"start": "Iniciar uma transmissão ao vivo",
"streamIdHelp": "O que é isso?",
"unavailableTitle": "Transmissão ao vivo indisponível"
},
"videoSIPGW": {
"busy": "Estamos trabalhando para liberar recursos. Por favor, tente novamente em alguns minutos.",
"busyTitle": "O serviço da sala está ocupado",
"errorInvite": "A conferência ainda não foi estabelecida. Por favor, tente mais tarde.",
"errorInviteTitle": "Erro no convite da sala",
"errorAlreadyInvited": "__displayName__ já convidado",
"errorInviteFailedTitle": "Convite para __displayName__ falhou",
"errorInviteFailed": "Estamos trabalhando para resolver o problema. Por favor, tente mais tarde.",
"pending": "__displayName__ foi convidado",
"serviceName": "Serviço da sala",
"unavailableTitle": "Serviço da sala indisponível"
},
"speakerStats": {
"hours": "__count__h",
@@ -444,46 +426,47 @@
"selectADevice": "Selecione um dispositivo",
"testAudio": "Testar o som"
},
"invite": {
"addPassword": "Adicionar uma senha",
"callNumber": "Ligar para __number__",
"enterID": "Digite o ID da Conferência: __conferenceID__ seguido de # em um telefone para participar",
"howToDialIn": "Para participar, use um dos números a seguir e o ID da conferência",
"hidePassword": "Esconder a senha",
"inviteTo": "Convidar pessoas para __conferenceName__",
"invitedYouTo": "__userName__ o convidou para a conferência __inviteURL__",
"invitePeople": "",
"locked": "Esta chamada está travada. Novos participantes precisam ter o link e digitar a senha para entrar.",
"showPassword": "Mostrar senha",
"unlocked": "Esta chamada está destravada. Qualquer novo participante com o link pode participar."
},
"videoStatus": {
"callQuality": "Qualidade da Chamada",
"hd": "HD",
"hdTooltip": "Ver vídeo em alta definição",
"highDefinition": "Alta definição (HD)",
"labelTooltipVideo": "Qualidade do vídeo atual",
"labelTooltipAudioOnly": "Modo somente de áudio habilitado",
"labelTooiltipNoVideo": "Sem vídeo",
"labelTooltipVideo": "Qualidade do vídeo atual",
"ld": "LD",
"ldTooltip": "Ver vídeo em baixa definição",
"lowDefinition": "Baixa definição (LD)",
"onlyAudioAvailable": "Somente áudio disponível",
"onlyAudioSupported": "Suportamos somente áudio neste navegador.",
"p2pEnabled": "Ponto-a-ponto habilitada",
"p2pVideoQualityDescription": "Em modo ponto-a-ponto, qualidade de chamadas recebidas podem somente ser modificada entre alta definição e áudio somente. Outras configurações não serão honradas até sair do ponto-a-ponto.",
"recHighDefinitionOnly": "Preferência para alta definição",
"sd": "SD",
"sdTooltip": "Ver vídeo em definição padrão",
"standardDefinition": "Definição padrão",
"qualityButtonTip": "Trocar a qualidade de vídeo recebido"
},
"dialOut": {
"dial": "Discar",
"dialOut": "",
"statusMessage": "está agora __status__",
"enterPhone": "Digite o número do telefone",
"phoneNotAllowed": "Oh, ainda não temos suporte para esse destino! Desculpe!"
"statusMessage": "está agora __status__"
},
"addPeople": {
"add": "Adicionar",
"add": "Convidar",
"countryNotSupported": "Ainda não suportamos este destino.",
"countryReminder": "Ligando fora dos EUA? Por favor, certifique-se de começar com o código do país!",
"disabled": "Você não pode convidar pessoas.",
"invite": "Convidar",
"loading": "Procurando por pessoas e números de telefone",
"loadingNumber": "Validando o número de telefone",
"loadingPeople": "Procurando por pessoas para convidar",
"noResults": "Nenhum resultado de busca correspondente",
"searchPlaceholder": "Encontrar por pessoas e salas para adicionar",
"title": "Adicionar pessoas à sua chamada",
"noValidNumbers": "Por favor, digite um número de telefone",
"notAvailable": "Você não pode convidar pessoas.",
"searchNumbers": "Adicionar números de telefones",
"searchPeople": "Pesquisar pessoas",
"searchPeopleAndNumbers": "Pesquisar por pessoas ou adicionar seus números de telefone",
"telephone": "Telefone: __number__",
"title": "Convide pessoas para sua reunião",
"failedToAdd": "Falha ao adicionar membros."
},
"inlineDialogFailure": {
@@ -493,15 +476,80 @@
"supportMsg": "Se isso continuar acontecendo, chegar a"
},
"deviceError": {
"cameraError": "",
"microphoneError": "",
"cameraError": "Falha ao acessar sua câmera",
"microphoneError": "Falha ao acessar seu microfone",
"cameraPermission": "Erro ao obter permissão para a câmera",
"microphonePermission": "Erro ao obter permissão para o microfone"
},
"feedback": {
"average": "Média",
"bad": "Ruim",
"good": "Boa",
"detailsLabel": "Nos conte mais sobre isso.",
"rateExperience": "Avalie sua experiência na reunião",
"veryBad": "Muito ruim",
"veryGood": "Muito boa"
},
"info": {
"copy": "Copiar link",
"invite": "Convidar em __app__",
"title": "Informações de acesso à chamada",
"addPassword": "Adicionar uma senha",
"cancelPassword": "Cancelar senha",
"conferenceURL": "Link:",
"country": "País",
"dialANumber": "Para participar da sua reunião, disque um desses números e insira o PIN: __conferenceID__#",
"dialInNumber": "Discar:",
"dialInConferenceID": "PIN:",
"dialInNotSupported": "Desculpe, a discagem não é atualmente suportada.",
"genericError": "Oops, alguma coisa deu errado.",
"inviteLiveStream": "Para ver a transmissão ao vivo da reunião, clique no link: __url__",
"invitePhone": "Para participar por telefone, disque __number__ e insira este PIN: __conferenceID__#",
"invitePhoneAlternatives": "Para ver mais números de telefone, clique neste link: __url__",
"inviteURL": "Para se juntar à reunião, clique neste link: __url__",
"liveStreamURL": "Transmissão ao vivo:",
"moreNumbers": "Mais números",
"noNumbers": "Sem números de discagem.",
"noPassword": "Nenhum",
"noRoom": "Nenhuma sala foi especificada para entrar.",
"numbers": "Números de discagem",
"password": "Senha:",
"title": "Compartilhar",
"tooltip": "Obtenha informações de acesso sobre a reunião"
},
"settingsView": {
"alertOk": "OK",
"alertTitle": "Atenção",
"alertURLText": "A URL digitada do servidor é inválida",
"conferenceSection": "Conferência",
"displayName": "Nome de exibição",
"email": "E-mail",
"header": "Configurações",
"profileSection": "Perfil",
"serverURL": "URL do servidor",
"startWithAudioMuted": "Iniciar sem áudio",
"startWithVideoMuted": "Iniciar sem vídeo"
},
"calendarSync": {
"later": "Depois",
"next": "Recebendo",
"nextMeeting": "próxima reunião",
"now": "Agora",
"permissionButton": "Abrir configurações",
"permissionMessage": "Permissão do calendário é requerida para listar suas reuniões na aplicação."
},
"recentList": {
"today": "Hoje",
"yesterday": "Ontem",
"earlier": "Mais cedo"
},
"sectionList": {
"pullToRefresh": "Puxe para atualizar"
},
"deepLinking": {
"title": "Iniciando sua reunião no __app__...",
"description": "Nada acontece? Estamos tentando iniciar sua reunião no aplicativo desktop __app__. Tente novamente ou inicie ele na aplicação web __app__.",
"tryAgainButton": "Tente novamente no desktop",
"launchWebButton": "Iniciar na web",
"appNotInstalled": "Você precisa do aplicativo móvel __app__ para participar da reunião no seu telefone.",
"downloadApp": "Baixe o Aplicativo",
"openApp": "Continue na aplicação"
}
}

View File

@@ -64,7 +64,7 @@
"roomnameHint": "Enter the name or URL of the room you want to join. You may make a name up, just let the people you are meeting know it so that they enter the same name.",
"sendFeedback": "Send feedback",
"terms": "Terms",
"title": "More secure, more flexible, and completely free video conferencing"
"title": "More secure, more flexible, and completely free video conferencing."
},
"startupoverlay": {
"policyText": " ",
@@ -78,9 +78,10 @@
"toolbar": {
"accessibilityLabel": {
"audioOnly": "Toggle audio only",
"audioRoute": "Select the audio route",
"audioRoute": "Select the sound device",
"callQuality": "Manage call quality",
"chat": "Toggle chat window",
"cc": "Toggle subtitles",
"document": "Toggle shared document",
"feedback": "Leave feedback",
"fullScreen": "Toggle full screen",
@@ -107,7 +108,7 @@
"audioonly": "Enable / Disable audio only mode (saves bandwidth)",
"audioOnlyOn": "Enable audio only mode (saves bandwidth)",
"audioOnlyOff": "Disable audio only mode",
"audioRoute": "Select the audio route",
"audioRoute": "Select the sound device",
"callQuality": "Manage call quality",
"enterFullScreen": "View full screen",
"exitFullScreen": "Exit full screen",
@@ -163,11 +164,15 @@
"selectMic": "Microphone",
"selectAudioOutput": "Audio output",
"followMe": "Everyone follows me",
"language": "Language",
"loggedIn": "Logged in as __name__",
"noDevice": "None",
"cameraAndMic": "Camera and microphone",
"moderator": "MODERATOR",
"moderator": "Moderator",
"more": "More",
"password": "SET PASSWORD",
"audioVideo": "AUDIO AND VIDEO"
"audioVideo": "AUDIO AND VIDEO",
"devices": "Devices"
},
"profile": {
"title": "Profile",
@@ -230,7 +235,6 @@
"focus": "Conference focus",
"focusFail": "__component__ not available - retry in __ms__ sec",
"grantedTo": "Moderator rights granted to __to__!",
"grantedToUnknown": "Moderator rights granted to $t(notify.somebody)!",
"muted": "You have started the conversation muted.",
"mutedTitle": "You're muted!",
"raisedHand": "Would like to speak.",
@@ -378,7 +382,8 @@
"shareYourScreenDisabled": "Screen sharing disabled.",
"shareYourScreenDisabledForGuest": "Guests can't screen share.",
"yourEntireScreen": "Your entire screen",
"applicationWindow": "Application window"
"applicationWindow": "Application window",
"transcribing": "Transcribing"
},
"email":
{
@@ -445,6 +450,16 @@
"unavailable": "Oops! The __serviceName__ is currently unavailable. We're working on resolving the issue. Please try again later.",
"unavailableTitle": "Recording unavailable"
},
"transcribing":
{
"pending" : "Preparing to transcribe the meeting...",
"off" : "Transcribing stopped",
"error": "Transcribing failed. Please try again.",
"failedToStart": "Transcribing failed to start",
"tr": "TR",
"labelToolTip": "The meeting is being transcribed",
"ccButtonTooltip": "Start / Stop showing subtitles"
},
"liveStreaming":
{
"busy": "We're working on freeing streaming resources. Please try again in a few minutes.",
@@ -464,7 +479,7 @@
"serviceName": "Live Streaming service",
"signIn": "Sign in with Google",
"signInCTA": "Sign in or enter your live stream key from YouTube.",
"start": "Start a livestream",
"start": "Start a live stream",
"streamIdHelp": "What's this?",
"unavailableTitle": "Live Streaming unavailable"
},
@@ -608,12 +623,10 @@
"now": "Now",
"ongoingMeeting": "ongoing meeting",
"permissionButton": "Open settings",
"permissionMessage": "Calendar permission is required to list your meetings in the app."
"permissionMessage": "The Calendar permission is required to see your meetings in the app."
},
"recentList": {
"today": "Today",
"yesterday": "Yesterday",
"earlier": "Earlier"
"joinPastMeeting": "Join A Past Meeting"
},
"sectionList": {
"pullToRefresh": "Pull to refresh"
@@ -629,16 +642,28 @@
},
"presenceStatus": {
"invited": "Invited",
"ringing": "Ringing",
"calling": "Calling",
"initializingCall": "Initializing Call",
"ringing": "Ringing...",
"calling": "Calling...",
"initializingCall": "Initializing Call...",
"connected": "Connected",
"connecting": "Connecting",
"connecting2": "Connecting*",
"connecting": "Connecting...",
"connecting2": "Connecting*...",
"disconnected": "Disconnected",
"busy": "Busy",
"rejected": "Rejected",
"ignored": "Ignored",
"expired": "Expired"
},
"dateUtils": {
"today": "Today",
"yesterday": "Yesterday",
"earlier": "Earlier"
},
"incomingCall": {
"answer": "Answer",
"audioCallTitle": "Incoming call",
"decline": "Dismiss",
"productLabel": "from Jitsi Meet",
"videoCallTitle": "Incoming video call"
}
}

View File

@@ -1,7 +1,6 @@
/* eslint-disable no-unused-vars, no-var */
// Logging configuration
// XXX When making any changes to this file make sure to also update it's React
// version at ./react/features/base/logging/reducer.js !!!
var loggingConfig = {
// default log level for the app and lib-jitsi-meet
defaultLogLevel: 'trace',
@@ -11,9 +10,17 @@ var loggingConfig = {
// The following are too verbose in their logging with the
// {@link #defaultLogLevel}:
'modules/RTC/TraceablePeerConnection.js': 'info',
'modules/statistics/CallStats.js': 'info',
'modules/xmpp/strophe.util.js': 'log',
'modules/RTC/TraceablePeerConnection.js': 'info'
'modules/xmpp/strophe.util.js': 'log'
};
/* eslint-enable no-unused-vars, no-var */
// XXX Web/React server-includes logging_config.js into index.html.
// Mobile/react-native requires it in react/features/base/logging. For the
// purposes of the latter, (try to) export loggingConfig. The following
// detection of a module system is inspired by webpack.
typeof module === 'object'
&& typeof exports === 'object'
&& (module.exports = loggingConfig);

View File

@@ -112,9 +112,21 @@ function initCommands() {
const { name } = request;
switch (name) {
case 'invite':
case 'invite': // eslint-disable-line no-case-declarations
const { invitees } = request;
if (!Array.isArray(invitees) || invitees.length === 0) {
callback({
error: new Error('Unexpected format of invitees')
});
break;
}
// The store should be already available because API.init is called
// on appWillMount action.
APP.store.dispatch(
invite(request.invitees))
invite(invitees, true))
.then(failedInvitees => {
let error;
let result;
@@ -363,6 +375,24 @@ class API {
});
}
/**
* Notify external application (if API is enabled) that user changed their
* email.
*
* @param {string} id - User id.
* @param {string} email - The new email of the participant.
* @returns {void}
*/
notifyEmailChanged(
id: string,
{ email }: Object) {
this._sendEvent({
name: 'email-change',
email,
id
});
}
/**
* Notify external application (if API is enabled) that the conference has
* been joined.

View File

@@ -40,6 +40,7 @@ const events = {
'audio-availability-changed': 'audioAvailabilityChanged',
'audio-mute-status-changed': 'audioMuteStatusChanged',
'display-name-change': 'displayNameChange',
'email-change': 'emailChange',
'feedback-submitted': 'feedbackSubmitted',
'incoming-message': 'incomingMessage',
'outgoing-message': 'outgoingMessage',
@@ -237,7 +238,9 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
}
})
});
this._invitees = invitees;
if (Array.isArray(invitees) && invitees.length > 0) {
this.invite(invitees);
}
this._isLargeVideoVisible = true;
this._numberOfParticipants = 0;
this._participants = {};
@@ -368,9 +371,6 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
switch (name) {
case 'video-conference-joined':
if (this._invitees) {
this.invite(this._invitees);
}
this._myUserID = userID;
this._participants[userID] = {
avatarURL: data.avatarURL
@@ -398,6 +398,14 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
}
break;
}
case 'email-change': {
const user = this._participants[userID];
if (user) {
user.email = data.email;
}
break;
}
case 'avatar-changed': {
const user = this._participants[userID];
@@ -591,6 +599,10 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
* @returns {Promise} - Resolves on success and rejects on failure.
*/
invite(invitees) {
if (!Array.isArray(invitees) || invitees.length === 0) {
return Promise.reject(new TypeError('Invalid Argument'));
}
return this._transport.sendRequest({
name: 'invite',
invitees
@@ -633,6 +645,18 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
return displayName;
}
/**
* Returns the email of a participant.
*
* @param {string} participantId - The id of the participant.
* @returns {string} The email.
*/
getEmail(participantId) {
const { email } = this._participants[participantId] || {};
return email;
}
/**
* Returns the formatted display name of a participant.
*

View File

@@ -15,12 +15,7 @@ import SharedVideoManager from './shared_video/SharedVideo';
import VideoLayout from './videolayout/VideoLayout';
import Filmstrip from './videolayout/Filmstrip';
import Profile from './side_pannels/profile/Profile';
import {
openDeviceSelectionDialog
} from '../../react/features/device-selection';
import { updateDeviceList } from '../../react/features/base/devices';
import { JitsiTrackErrors } from '../../react/features/base/lib-jitsi-meet';
import {
getLocalParticipant,
@@ -33,7 +28,6 @@ import {
setNotificationsEnabled,
showWarningNotification
} from '../../react/features/notifications';
import { shouldShowOnlyDeviceSelection } from '../../react/features/settings';
import {
dockToolbox,
setToolboxEnabled,
@@ -97,22 +91,6 @@ const UIListeners = new Map([
], [
UIEvents.TOGGLE_CHAT,
() => UI.toggleChat()
], [
UIEvents.TOGGLE_SETTINGS,
() => {
// Opening of device selection is special-cased as it is a dialog
// opened through a button in settings and not directly displayed in
// settings itself. As it is not useful to only have a settings menu
// with a button to open a dialog, open the dialog directly instead.
if (shouldShowOnlyDeviceSelection()) {
APP.store.dispatch(openDeviceSelectionDialog());
} else {
UI.toggleSidePanel('settings_container');
}
}
], [
UIEvents.TOGGLE_PROFILE,
() => UI.toggleSidePanel('profile_container')
], [
UIEvents.TOGGLE_FILMSTRIP,
() => UI.handleToggleFilmstrip()
@@ -216,22 +194,10 @@ UI.changeDisplayName = function(id, displayName) {
VideoLayout.onDisplayNameChanged(id, displayName);
if (APP.conference.isLocalId(id) || id === 'localVideoContainer') {
Profile.changeDisplayName(displayName);
Chat.setChatConversationMode(Boolean(displayName));
}
};
/**
* Shows/hides the indication about local connection being interrupted.
*
* @param {boolean} isInterrupted <tt>true</tt> if local connection is
* currently in the interrupted state or <tt>false</tt> if the connection
* is fine.
*/
UI.showLocalConnectionInterrupted = function(isInterrupted) {
VideoLayout.showLocalConnectionInterrupted(isInterrupted);
};
/**
* Sets the "raised hand" status for a participant.
*
@@ -268,7 +234,7 @@ UI.setLocalRaisedHandStatus
*/
UI.initConference = function() {
const { getState } = APP.store;
const { email, id, name } = getLocalParticipant(getState);
const { id, name } = getLocalParticipant(getState);
// Update default button states before showing the toolbar
// if local role changes buttons state will be again updated.
@@ -282,11 +248,6 @@ UI.initConference = function() {
UI.changeDisplayName('localVideoContainer', displayName);
}
// Make sure we configure our avatar id, before creating avatar for us
if (email) {
UI.setUserEmail(id, email);
}
// FollowMe attempts to copy certain aspects of the moderator's UI into the
// other participants' UI. Consequently, it needs (1) read and write access
// to the UI (depending on the moderator role of the local participant) and
@@ -294,16 +255,6 @@ UI.initConference = function() {
followMeHandler = new FollowMe(APP.conference, UI);
};
UI.mucJoined = function() {
VideoLayout.mucJoined();
// Update local video now that a conference is joined a user ID should be
// set.
UI.changeDisplayName(
'localVideoContainer',
APP.conference.getLocalDisplayName());
};
/** *
* Handler for toggling filmstrip
*/
@@ -367,17 +318,8 @@ UI.start = function() {
// Initialize side panels
SidePanels.init(eventEmitter);
// TODO: remove this class once the old toolbar has been removed. This
// class is set so that any CSS changes needed to adjust elements
// outside of the new toolbar can be scoped to just the app with the new
// toolbar enabled.
$('body').addClass('use-new-toolbox');
}
interfaceConfig.VERTICAL_FILMSTRIP
&& $('body').addClass('vertical-filmstrip');
document.title = interfaceConfig.APP_NAME;
};
@@ -442,13 +384,6 @@ UI.addLocalStream = track => {
}
};
/**
* Show remote stream on UI.
* @param {JitsiTrack} track stream to show
*/
UI.addRemoteStream = track => VideoLayout.onRemoteStreamAdded(track);
/**
* Removed remote stream from UI.
* @param {JitsiTrack} track stream to remove
@@ -492,9 +427,6 @@ UI.addUser = function(user) {
APP.store.dispatch(showParticipantJoinedNotification(displayName));
}
// Configure avatar
UI.setUserEmail(id);
// set initial display name
if (displayName) {
UI.changeDisplayName(id, displayName);
@@ -538,20 +470,13 @@ UI.updateUserRole = user => {
const displayName = user.getDisplayName();
if (displayName) {
messageHandler.participantNotification(
displayName,
'notify.somebody',
'connected',
'notify.grantedTo',
{ to: UIUtil.escapeHtml(displayName) });
} else {
messageHandler.participantNotification(
'',
'notify.somebody',
'connected',
'notify.grantedToUnknown');
}
messageHandler.participantNotification(
displayName,
'notify.somebody',
'connected',
'notify.grantedTo',
{ to: displayName
? UIUtil.escapeHtml(displayName) : '$t(notify.somebody)' });
};
/**
@@ -561,7 +486,10 @@ UI.updateUserRole = user => {
* @param {string} status - The new status.
*/
UI.updateUserStatus = (user, status) => {
if (!status) {
const reduxState = APP.store.getState() || {};
const { calleeInfoVisible } = reduxState['features/invite'] || {};
if (!status || calleeInfoVisible) {
return;
}
@@ -739,17 +667,6 @@ UI.showToolbar = timeout => APP.store.dispatch(showToolbox(timeout));
// Used by torture.
UI.dockToolbar = dock => APP.store.dispatch(dockToolbox(dock));
/**
* Update user email.
* @param {string} id user id
* @param {string} email user email
*/
UI.setUserEmail = function(id, email) {
if (APP.conference.isLocalId(id)) {
Profile.changeEmail(email);
}
};
/**
* Updates the displayed avatar for participant.
*
@@ -832,18 +749,6 @@ UI.hideStats = function() {
VideoLayout.hideStats();
};
/**
* Mark video as interrupted or not.
* @param {boolean} interrupted if video is interrupted
*/
UI.markVideoInterrupted = function(interrupted) {
if (interrupted) {
VideoLayout.onVideoInterrupted();
} else {
VideoLayout.onVideoRestored();
}
};
/**
* Add chat message.
* @param {string} from user id
@@ -880,25 +785,6 @@ UI.notifyFocusDisconnected = function(focus, retrySec) {
);
};
/**
* Updates auth info on the UI.
* @param {boolean} isAuthEnabled if authentication is enabled
* @param {string} [login] current login
*/
UI.updateAuthInfo = function(isAuthEnabled, login) {
const showAuth = isAuthEnabled && UIUtil.isAuthenticationEnabled();
const loggedIn = Boolean(login);
Profile.showAuthenticationButtons(showAuth);
if (showAuth) {
Profile.setAuthenticatedIdentity(login);
Profile.showLoginButton(!loggedIn);
Profile.showLogoutButton(loggedIn);
}
};
/**
* Notifies interested listeners that the raise hand property has changed.
*
@@ -911,10 +797,8 @@ UI.onLocalRaiseHandChanged = function(isRaisedHand) {
/**
* Update list of available physical devices.
* @param {object[]} devices new list of available devices
*/
UI.onAvailableDevicesChanged = function(devices) {
APP.store.dispatch(updateDeviceList(devices));
UI.onAvailableDevicesChanged = function() {
APP.conference.updateAudioIconEnabled();
APP.conference.updateVideoIconEnabled();
};

View File

@@ -305,7 +305,7 @@ export default class SharedVideoManager {
// JitsiMeetLogStorage.
conference: APP.conference._room,
id: self.url,
isBot: true,
isFakeParticipant: true,
name: 'YouTube'
}));

View File

@@ -50,10 +50,14 @@ SharedVideoThumb.prototype.createContainer = function(spanId) {
displayNameContainer.className = 'displayNameContainer';
container.appendChild(displayNameContainer);
const remotes = document.getElementById('filmstripRemoteVideosContainer');
const remoteVideosContainer
= document.getElementById('filmstripRemoteVideosContainer');
const localVideoContainer
= document.getElementById('localVideoTileViewContainer');
remoteVideosContainer.insertBefore(container, localVideoContainer);
return remotes.appendChild(container);
return container;
};
/**

View File

@@ -1,6 +1,4 @@
import Chat from './chat/Chat';
import SettingsMenu from './settings/SettingsMenu';
import Profile from './profile/Profile';
import { isButtonEnabled } from '../../../react/features/toolbox';
const SidePanels = {
@@ -9,16 +7,6 @@ const SidePanels = {
if (isButtonEnabled('chat')) {
Chat.init(eventEmitter);
}
// Initialize settings
if (isButtonEnabled('settings')) {
SettingsMenu.init(eventEmitter);
}
// Initialize profile
if (isButtonEnabled('profile')) {
Profile.init(eventEmitter);
}
}
};

View File

@@ -43,6 +43,10 @@ const htmlStr = `
function initHTML() {
$(`#${sidePanelsContainerId}`)
.append(htmlStr);
// make sure we translate the panel, as adding it can be after i18n
// library had initialized and translated already present html
APP.translation.translateElement($(`#${sidePanelsContainerId}`));
}
/**

View File

@@ -1,194 +0,0 @@
/* global $, APP */
import UIUtil from '../../util/UIUtil';
import UIEvents from '../../../../service/UI/UIEvents';
import {
createProfilePanelButtonEvent,
sendAnalytics
} from '../../../../react/features/analytics';
const sidePanelsContainerId = 'sideToolbarContainer';
const htmlStr = `
<div id='profile_container' class='sideToolbarContainer__inner'>
<div class='title' data-i18n='profile.title'></div>
<div class='sideToolbarBlock first'>
<label class='first' data-i18n='profile.setDisplayNameLabel'>
</label>
<input class='input-control' type='text' id='setDisplayName'
data-i18n='[placeholder]settings.name'>
</div>
<div class='sideToolbarBlock'>
<label data-i18n='profile.setEmailLabel'></label>
<input id='setEmail' type='text' class='input-control'
data-i18n='[placeholder]profile.setEmailInput'>
</div>
<div id='profile_auth_container'
class='sideToolbarBlock auth_container'>
<p data-i18n='toolbar.authenticate'></p>
<ul>
<li id='profile_auth_identity'></li>
<li id='profile_button_login'>
<a class='authButton' data-i18n='toolbar.login'></a>
</li>
<li id='profile_button_logout'>
<a class='authButton' data-i18n='toolbar.logout'></a>
</li>
</ul>
</div>
</div>`;
/**
*
*/
function initHTML() {
$(`#${sidePanelsContainerId}`)
.append(htmlStr);
// make sure we translate the panel, as adding it can be after i18n
// library had initialized and translated already present html
APP.translation.translateElement($(`#${sidePanelsContainerId}`));
}
export default {
init(emitter) {
initHTML();
const settings = APP.store.getState()['features/base/settings'];
/**
* Updates display name.
*
* @returns {void}
*/
function updateDisplayName() {
emitter.emit(UIEvents.NICKNAME_CHANGED, $('#setDisplayName').val());
}
$('#setDisplayName')
.val(settings.displayName)
.keyup(event => {
if (event.keyCode === 13) { // enter
updateDisplayName();
}
})
.focusout(updateDisplayName);
/**
* Updates the email.
*
* @returns {void}
*/
function updateEmail() {
emitter.emit(UIEvents.EMAIL_CHANGED, $('#setEmail').val());
}
$('#setEmail')
.val(settings.email)
.keyup(event => {
if (event.keyCode === 13) { // enter
updateEmail();
}
})
.focusout(updateEmail);
/**
*
*/
function loginClicked() {
sendAnalytics(createProfilePanelButtonEvent('login.button'));
emitter.emit(UIEvents.AUTH_CLICKED);
}
$('#profile_button_login').click(loginClicked);
/**
*
*/
function logoutClicked() {
const titleKey = 'dialog.logoutTitle';
const msgKey = 'dialog.logoutQuestion';
sendAnalytics(createProfilePanelButtonEvent('logout.button'));
// Ask for confirmation
APP.UI.messageHandler.openTwoButtonDialog({
titleKey,
msgKey,
leftButtonKey: 'dialog.Yes',
submitFunction(evt, yes) {
if (yes) {
emitter.emit(UIEvents.LOGOUT);
}
}
});
}
$('#profile_button_logout').click(logoutClicked);
},
/**
* Check if settings menu is visible or not.
* @returns {boolean}
*/
isVisible() {
return UIUtil.isVisible(document.getElementById('profile_container'));
},
/**
* Change user display name in the settings menu.
* @param {string} newDisplayName
*/
changeDisplayName(newDisplayName) {
$('#setDisplayName').val(newDisplayName);
},
/**
* Change the value of the field for the user email.
* @param {string} email the new value that will be displayed in the field.
*/
changeEmail(email) {
$('#setEmail').val(email);
},
/**
* Shows or hides authentication related buttons
* @param {boolean} show <tt>true</tt> to show or <tt>false</tt> to hide
*/
showAuthenticationButtons(show) {
const id = 'profile_auth_container';
UIUtil.setVisible(id, show);
},
/**
* Shows/hides login button.
* @param {boolean} show <tt>true</tt> to show or <tt>false</tt> to hide
*/
showLoginButton(show) {
const id = 'profile_button_login';
UIUtil.setVisible(id, show);
},
/**
* Shows/hides logout button.
* @param {boolean} show <tt>true</tt> to show or <tt>false</tt> to hide
*/
showLogoutButton(show) {
const id = 'profile_button_logout';
UIUtil.setVisible(id, show);
},
/**
* Displays user's authenticated identity name (login).
* @param {string} authIdentity identity name to be displayed.
*/
setAuthenticatedIdentity(authIdentity) {
const id = 'profile_auth_identity';
UIUtil.setVisible(id, Boolean(authIdentity));
$(`#${id}`).text(authIdentity ? authIdentity : '');
}
};

View File

@@ -1,52 +0,0 @@
/* global $, APP, interfaceConfig */
/* eslint-disable no-unused-vars */
import React from 'react';
import ReactDOM from 'react-dom';
import { I18nextProvider } from 'react-i18next';
import { Provider } from 'react-redux';
import { i18next } from '../../../../react/features/base/i18n';
import {
SettingsMenu,
isSettingEnabled
} from '../../../../react/features/settings';
import UIUtil from '../../util/UIUtil';
/* eslint-enable no-unused-vars */
export default {
init() {
const settingsMenuContainer = document.createElement('div');
settingsMenuContainer.id = 'settings_container';
settingsMenuContainer.className = 'sideToolbarContainer__inner';
$('#sideToolbarContainer').append(settingsMenuContainer);
const props = {
showDeviceSettings: isSettingEnabled('devices'),
showLanguageSettings: isSettingEnabled('language'),
showModeratorSettings: isSettingEnabled('moderator'),
showTitles: interfaceConfig.SETTINGS_SECTIONS.length > 1
};
ReactDOM.render(
<Provider store = { APP.store }>
<I18nextProvider i18n = { i18next }>
<SettingsMenu { ...props } />
</I18nextProvider>
</Provider>,
settingsMenuContainer
);
},
/**
* Check if settings menu is visible or not.
* @returns {boolean}
*/
isVisible() {
return UIUtil.isVisible(document.getElementById('settings_container'));
}
};

View File

@@ -1,4 +1,4 @@
/* global $, APP, JitsiMeetJS */
/* global $, APP */
/* eslint-disable no-unused-vars */
import React from 'react';
import ReactDOM from 'react-dom';
@@ -29,14 +29,6 @@ import AudioLevels from '../audio_levels/AudioLevels';
const DESKTOP_CONTAINER_TYPE = 'desktop';
/**
* The time interval in milliseconds to check the video resolution of the video
* being displayed.
*
* @type {number}
*/
const VIDEO_RESOLUTION_POLL_INTERVAL = 2000;
/**
* Manager for all Large containers.
*/
@@ -103,30 +95,15 @@ export default class LargeVideoManager {
= this._onVideoResolutionUpdate.bind(this);
this.videoContainer.addResizeListener(this._onVideoResolutionUpdate);
if (!JitsiMeetJS.util.RTCUIHelper.isResizeEventSupported()) {
/**
* An interval for polling if the displayed video resolution is or
* is not high-definition. For browsers that do not support video
* resize events, polling is the fallback.
*
* @private
* @type {timeoutId}
*/
this._updateVideoResolutionInterval = window.setInterval(
this._onVideoResolutionUpdate,
VIDEO_RESOLUTION_POLL_INTERVAL);
}
}
/**
* Stops any polling intervals on the instance and removes any
* listeners registered on child components, including React Components.
* Removes any listeners registered on child components, including
* React Components.
*
* @returns {void}
*/
destroy() {
window.clearInterval(this._updateVideoResolutionInterval);
this.videoContainer.removeResizeListener(
this._onVideoResolutionUpdate);
@@ -451,7 +428,9 @@ export default class LargeVideoManager {
ReactDOM.render(
<Provider store = { APP.store }>
<I18nextProvider i18n = { i18next }>
<PresenceLabel participantID = { id } />
<PresenceLabel
participantID = { id }
className = 'presence-label' />
</I18nextProvider>
</Provider>,
presenceLabelContainer.get(0));

View File

@@ -258,11 +258,8 @@ LocalVideo.prototype._onContainerClick = function(event) {
= $source.parents('.displayNameContainer').length > 0;
const clickedOnPopover = $source.parents('.popover').length > 0
|| classList.contains('popover');
const ignoreClick = clickedOnDisplayName || clickedOnPopover;
// FIXME: with Temasys plugin event arg is not an event, but the clicked
// object itself, so we have to skip this call
if (event.stopPropagation && !ignoreClick) {
event.stopPropagation();
}

View File

@@ -158,16 +158,13 @@ RemoteVideo.prototype._generatePopupContent = function() {
}
}
let initialVolumeValue, onVolumeChange;
// Feature check for volume setting as temasys objects cannot adjust volume.
if (this._canSetAudioVolume()) {
initialVolumeValue = this._getAudioElement().volume;
onVolumeChange = this._setAudioVolume;
}
const initialVolumeValue
= this._audioStreamElement && this._audioStreamElement.volume;
const onVolumeChange = this._setAudioVolume;
const { isModerator } = APP.conference;
const participantID = this.id;
const menuPosition = interfaceConfig.VERTICAL_FILMSTRIP
? 'left bottom' : 'top center';
ReactDOM.render(
<Provider store = { APP.store }>
@@ -177,6 +174,7 @@ RemoteVideo.prototype._generatePopupContent = function() {
initialVolumeValue = { initialVolumeValue }
isAudioMuted = { this.isAudioMuted }
isModerator = { isModerator }
menuPosition = { menuPosition }
onMenuDisplay
= {this._onRemoteVideoMenuDisplay.bind(this)}
onRemoteControlToggle = { onRemoteControlToggle }
@@ -267,35 +265,14 @@ RemoteVideo.prototype._stopRemoteControl = function() {
this.updateRemoteVideoMenu();
};
/**
* Get the remote participant's audio element.
*
* @returns {Element} audio element
*/
RemoteVideo.prototype._getAudioElement = function() {
return this._audioStreamElement;
};
/**
* Check if the remote participant's audio can have its volume adjusted.
*
* @returns {boolean} true if the volume can be adjusted.
*/
RemoteVideo.prototype._canSetAudioVolume = function() {
const audioElement = this._getAudioElement();
return audioElement && audioElement.volume !== undefined;
};
/**
* Change the remote participant's volume level.
*
* @param {int} newVal - The value to set the slider to.
*/
RemoteVideo.prototype._setAudioVolume = function(newVal) {
if (this._canSetAudioVolume()) {
this._getAudioElement().volume = newVal;
if (this._audioStreamElement) {
this._audioStreamElement.volume = newVal;
}
};
@@ -526,23 +503,18 @@ RemoteVideo.prototype.addRemoteStreamElement = function(stream) {
return;
}
let streamElement = SmallVideo.createStreamElement(stream);
const streamElement = SmallVideo.createStreamElement(stream);
// Put new stream element always in front
UIUtils.prependChild(this.container, streamElement);
// If we hide element when Temasys plugin is used then
// we'll never receive 'onplay' event and other logic won't work as expected
// NOTE: hiding will not have effect when Temasys plugin is in use, as
// calling attach will show it back
$(streamElement).hide();
// If the container is currently visible
// we attach the stream to the element.
if (!isVideo || (this.container.offsetParent !== null && isVideo)) {
this.waitForPlayback(streamElement, stream);
streamElement = stream.attach(streamElement);
stream.attach(streamElement);
}
if (!isVideo) {
@@ -604,7 +576,9 @@ RemoteVideo.prototype.addPresenceLabel = function() {
ReactDOM.render(
<Provider store = { APP.store }>
<I18nextProvider i18n = { i18next }>
<PresenceLabel participantID = { this.id } />
<PresenceLabel
participantID = { this.id }
className = 'presence-label' />
</I18nextProvider>
</Provider>,
presenceLabelContainer);
@@ -671,10 +645,14 @@ RemoteVideo.createContainer = function(spanId) {
<div class ='presence-label-container'></div>
<span class = 'remotevideomenu'></span>`;
const remotes = document.getElementById('filmstripRemoteVideosContainer');
const remoteVideosContainer
= document.getElementById('filmstripRemoteVideosContainer');
const localVideoContainer
= document.getElementById('localVideoTileViewContainer');
remoteVideosContainer.insertBefore(container, localVideoContainer);
return remotes.appendChild(container);
return container;
};
export default RemoteVideo;

View File

@@ -1,4 +1,4 @@
/* global $, APP, JitsiMeetJS, interfaceConfig */
/* global $, APP, interfaceConfig */
/* eslint-disable no-unused-vars */
import React from 'react';
@@ -34,8 +34,6 @@ const logger = require('jitsi-meet-logger').getLogger(__filename);
import UIUtil from '../util/UIUtil';
import UIEvents from '../../../service/UI/UIEvents';
const RTCUIHelper = JitsiMeetJS.util.RTCUIHelper;
/**
* Display mode constant used when video is being displayed on the small video.
* @type {number}
@@ -116,7 +114,8 @@ function SmallVideo(VideoLayout) {
* @private
* @type {boolean}
*/
this._showConnectionIndicator = true;
this._showConnectionIndicator
= !interfaceConfig.CONNECTION_INDICATOR_DISABLED;
/**
* Whether or not the dominant speaker indicator should be displayed.
@@ -240,8 +239,7 @@ SmallVideo.createStreamElement = function(stream) {
element.setAttribute('muted', 'true');
}
RTCUIHelper.setAutoPlay(element, true);
element.autoplay = true;
element.id = SmallVideo.getStreamElementID(stream);
return element;
@@ -438,7 +436,7 @@ SmallVideo.prototype.removeModeratorIndicator = function() {
* array (after checking its length of course!).
*/
SmallVideo.prototype.selectVideoElement = function() {
return $(RTCUIHelper.findVideoElement(this.container));
return $($(this.container).find('video')[0]);
};
/**
@@ -550,7 +548,7 @@ SmallVideo.prototype.isVideoPlayable = function() {
SmallVideo.prototype.selectDisplayMode = function() {
// Display name is always and only displayed when user is on the stage
if (this.isCurrentlyOnLargeVideo()) {
return this.isVideoPlayable()
return this.isVideoPlayable() && !APP.conference.isAudioOnly()
? DISPLAY_BLACKNESS_WITH_NAME : DISPLAY_AVATAR_WITH_NAME;
} else if (this.isVideoPlayable()
&& this.selectVideoElement().length

View File

@@ -7,8 +7,7 @@ import ReactDOM from 'react-dom';
import { browser } from '../../../react/features/base/lib-jitsi-meet';
import {
ORIENTATION,
LargeVideoBackground,
LargeVideoBackgroundCanvas
LargeVideoBackground
} from '../../../react/features/large-video';
/* eslint-enable no-unused-vars */
@@ -187,7 +186,6 @@ function getDesktopVideoPosition(videoWidth, videoHeight, videoSpaceWidth) {
* Container for user video.
*/
export class VideoContainer extends LargeContainer {
// FIXME: With Temasys we have to re-select everytime
/**
*
*/
@@ -278,10 +276,6 @@ export class VideoContainer extends LargeContainer {
this.wasVideoRendered = true;
}.bind(this);
// This does not work with Temasys plugin - has to be a property to be
// copied between new <object> elements
// this.$video.on('play', onPlay);
this.$video[0].onplaying = onPlayingCallback;
/**
@@ -291,7 +285,6 @@ export class VideoContainer extends LargeContainer {
*/
this._resizeListeners = new Set();
// As of May 16, 2017, temasys does not support resize events.
this.$video[0].onresize = this._onResize.bind(this);
}
@@ -606,6 +599,8 @@ export class VideoContainer extends LargeContainer {
* <video> elements with plugin <object> tag. In Safari jQuery is
* unable to store values on this plugin object which breaks all
* animation effects performed on it directly.
*
* TODO: refactor this since Temasys is no longer supported.
*/
show() {
// its already visible
@@ -689,21 +684,16 @@ export class VideoContainer extends LargeContainer {
*/
_updateBackground() {
// Do not the background display on browsers that might experience
// performance issues from the presence of the background.
if (interfaceConfig._BACKGROUND_BLUR === 'off'
// performance issues from the presence of the background or if
// explicitly disabled.
if (interfaceConfig.DISABLE_VIDEO_BACKGROUND
|| browser.isFirefox()
|| browser.isSafariWithWebrtc()
|| browser.isTemasysPluginUsed()) {
|| browser.isSafariWithWebrtc()) {
return;
}
// eslint-disable-next-line no-unused-vars
const Background = interfaceConfig._BACKGROUND_BLUR === 'canvas'
? LargeVideoBackgroundCanvas
: LargeVideoBackground;
ReactDOM.render(
<Background
<LargeVideoBackground
hidden = { this._hideBackground }
mirror = {
this.stream

Some files were not shown because too many files have changed in this diff Show More