Compare commits

...

141 Commits

Author SHA1 Message Date
Zoltan Bettenbuk
4f8fd1019b Separate local thumbnail in filmstrip (#2848)
* Separate local thumbnail in filmstrip

* style(Filmstrip.native): utilize full line length
2018-04-26 07:44:23 -05:00
hristoterezov
f14095ecfc feat(deep_linking): add analytics
In order to be able to add analytics to the deep-linking pages the
lib-jitsi-meet initialization has been moved so it happens earlier.

The introduced `initPromise` will eventually disappear, once conference is
migrated into React and / or support for Temasys is dropped. At that stage, it
can be turned into a sync function which all platforms share.
2018-04-26 10:11:34 +02:00
zbettenbuk
6947926494 Add dynamic move threshold to pnz touch detection 2018-04-25 16:35:22 -05:00
hristoterezov
784d94b30f fix(deeplinking): logo image 2018-04-25 14:19:08 -05:00
Saúl Ibarra Corretgé
1d7e0845aa [RN] use the share button if the invite button is not enabled 2018-04-25 18:58:06 +02:00
Ryan Peck
f64c13d4b7 [RN] add support for inviting participants during a call on mobile
* Button conditionally shown based on if the feature is enabled and available
* Hooks for launching the invite UI (delegates to the native layer)
* Hooks for using the search and dial out checks from the native layer (calls back into JS)
* Hooks for handling sending invites and passing any failures back to the native layer
* Android and iOS handling for those hooks

Author: Ryan Peck <rpeck@atlassian.com>
Author: Eric Brynsvold <ebrynsvold@atlassian.com>
2018-04-25 18:58:06 +02:00
George Politis
4e36127dc7 config: Whitelists enable{Remb,Tcc} and minParticipants. 2018-04-24 16:01:23 -05:00
Дамян Минков
8b1aff5512 Adds in memory log storage, to be used while testing. (#2858)
* Adds in memory log storage, to be used while testing.

Enabling it only when config.debug is set, a configuration provided by jitsi-meet-torture.

* Moves to using config.testing.testMode property for logs storage.

* Fixes comments.
2018-04-24 13:56:54 -05:00
Saúl Ibarra Corretgé
d7103c1c4c feat(UI): simplify code (#2847)
Avoid branching on the same condition and remove unneeded showToolbar call on
filmstrip only mode.
2018-04-24 11:36:56 -07:00
paweldomas
9cbcbf6e26 fix(PictureInPictureModule): catch the RuntimeException
Activity.enterPictureInPictureMode can fail for a couple of reasons
mentioned in the JSDoc:

"The system may disallow entering picture-in-picture in various cases,
including when the activity is not visible, if the screen is locked or
if the user has an activity pinned."

It seems to be safe to assume that those cases will be caught by
a RuntimeException handler (only RuntimeExceptions can be left without
explicit catch block).

Anyway the root cause for problems is the fact that the current process
for going to the picture in picture mode is not synchronised with
Activity's lifecycle. On Activity's "userLeaveHint" callback we dispatch
async task to the JS code which only then after dispatching some more
stuff eventually call native method that enter PiP. In case we spend too
much time on the JS side and the Activity goes to PAUSED state the call
will fail with IllegalStatException: "activity is not visible",
"activity is paused" etc. This means with this fix the app will not
crash, but we'll see it sometimes not go to the PiP mode as expected.
2018-04-22 00:41:18 -05:00
virtuacoplenny
2c4a3b0f60 Show the YouTube live stream URL (#2837)
* feat(recording): show the YouTube live stream URL

- From the start live stream dialog, push up the broadcast ID
  of the chosen broadcast. It is assumed the ID can be used to
  create the YouTube link.
- Listen for lib-jitsi-meet to emit updates of the known live
  stream URL, shove it into redux, and have InfoDialog display
  it.

* ref(info): pass in dial in and live stream url

Passing these values in should trigger AtlasKit InlineDialog
to re-render and reposition itself.

* ref(info): use conference existence as trigger for autoshowing dialog

* feat(info): add live stream link to invite copy

* Revert "ref(info): use conference existence as trigger for autoshowing dialog"

This reverts commit 1072102267.

* hidden -> url

* _onClickHiddenURL -> _onClickURLText
2018-04-20 10:28:16 -07:00
Zoltan Bettenbuk
d82f172db8 Merge pull request #2846 from saghul/make-dev
misc: add helper make dev command
2018-04-20 14:08:42 +02:00
Saúl Ibarra Corretgé
169c47ac7f misc: add helper make dev command
Running webpack-dev-server is not enough, so add a helper which takes are of
doing the needful.
2018-04-20 11:37:28 +02:00
paweldomas
2af76ebcf9 fix(testing): add TestHint for LargeVideo
Since the main conference container is no longer "clickable" there must
be a way for clicking on the "large video". A clickable TestHint nested
in ParticipantView makes it easier for dealing with the fact that the
click handler is not always on the same component (required for the
pinch and zoom feature to work correctly).
2018-04-19 16:49:22 -05:00
paweldomas
6931b8f2fb feat(TestHint): add 'onPress' property
Allows to bind a click handler to a TestHint.

When a mobile test wants to click an UI element it must be able to
locate it through the accessibility layer. Now the problem with that is
that there is currently no uniform way for finding element on both iOS
and Android. This problem is solved by TestHint component which takes
an id parameter which then can be specified in the corresponding java
TestHint class in jitsi-meet-torture to easily find it. By being able to
add a click handler to a TestHint, it's possible to duplicate original
handler under nested TestHint and then find it easily on the torture
side.
2018-04-19 16:49:22 -05:00
paweldomas
adec8e6438 ref(TestHint): render only in test mode
Adds the logic to render TestHint only when the test mode is enabled
in order to be able to put independent TestHints in other places than
the TestConnectionInfo component.
2018-04-19 16:49:22 -05:00
paweldomas
382c548cf9 ref(testing): move 'testing' feature to base 2018-04-19 16:49:22 -05:00
Shuai Li
ff9820a61b [react-native-webrtc] ios: fix build when using use_frameworks! 2018-04-19 16:32:42 -05:00
zbettenbuk
1ab107b238 [react-native-calendar-events] Null check empty cursor 2018-04-19 15:35:57 -05:00
Saúl Ibarra Corretgé
2861d8d24e misc: remove dead code 🔥🔥🔥 (#2844)
- old toolbox actions
- chat command processor
- room subject handling
2018-04-19 10:24:16 -07:00
Дамян Минков
172342cac3 Updates lib-jitsi-meet to 3077772. (#2845)
Fixes custom callstats script param name.
2018-04-19 09:18:19 -07:00
Zoltan Bettenbuk
f8941c846a Merge pull request #2838 from saghul/fix-statusbar-ios
[iOS] Fix statusbar color while in a conference
2018-04-19 13:21:46 +02:00
Saúl Ibarra Corretgé
8daa13cd99 [iOS] Fix statusbar color while in a conference
Be explciit about the appearance we desire, since each mounted StaturBar
component will override the existing values. In this case, the problem was
caused because the default on iOS is dark, whereas it's light on Android.

Set it to light so it works consistently across both, which is what we want.
2018-04-19 10:56:09 +02:00
Leonard Kim
a86ca3f41c fix(toolbar): set recording icon size to prevent resizing flash
There is a slight moment when the recording icon is loading that
its container does not have width. Set the width of the container
so it doesn't collapse. Also, push it a little to the right so
it aligns better with other icons.
2018-04-18 13:57:53 -05:00
Leonard Kim
c029663b77 fix(toolbar): move chat counter and stop its pointer events
The chat counter needs to be moved out of the way of the chat
button. The counter started covering the button when all the
toolbar buttons were made smaller. Also, turning off the
counters pointer events should at least make the button
clickable if this ever happens again.
2018-04-18 13:57:45 -05:00
Lyubo Marinov
66bf5cf966 [RN] Avoid "pinch to zoom" onPress
It's too sensitive and most of the time I cannot perform an onPress. In
contrast, the builtin/default/standard onPress is noticeably more
forgiving. While we fix the sensitivity of "pinch to zoom", don't use
its onPress unless absolutely necessary i.e. use it only for desktop
streams.
2018-04-17 17:42:46 -05:00
zbettenbuk
63c165ee8b More generic way to refresh lists on the welcome screen 2018-04-17 17:42:46 -05:00
zbettenbuk
374e3ccf2c Properly gate calendar feature on-off 2018-04-17 17:41:12 -05:00
Leonard Kim
09482f053b ref(toolbar): remove main css for old toolbar 2018-04-17 20:22:00 +02:00
Leonard Kim
1e69dc93d6 ref(toolbar): kill Stateless Toolbar and Invite, Feedback, Profile buttons 2018-04-17 20:22:00 +02:00
zbettenbuk
008645568c Fix startAudioOnly and startWithVideoMuted collision on start from URL
Zoltan Bettenbuk suggested the following:

         const state = getState();

          if (desiredTypes.length === 0) {
 -            const { audio, video } = state['features/base/media'];
 -
 -            audio.muted || desiredTypes.push(MEDIA_TYPE.AUDIO);
 -            video.muted || desiredTypes.push(MEDIA_TYPE.VIDEO);
 +            const startAudioOnly = getPropertyValue(state, 'startAudioOnly');
 +            const startWithAudioMuted
 +                = getPropertyValue(state, 'startWithAudioMuted');
 +            const startWithVideoMuted
 +                = getPropertyValue(state, 'startWithVideoMuted');
 +
 +            if (!startWithAudioMuted) {
 +                desiredTypes.push(MEDIA_TYPE.AUDIO);
 +            }
 +            if (!startAudioOnly && !startWithVideoMuted) {
 +                desiredTypes.push(MEDIA_TYPE.VIDEO);
 +            }
          }

          const availableTypes

The final commit is really a different implementation of the same idea
but takes into account that the state of base/media already contains the
intent of the URL and notices the delay in the realization of the
background app state.

Additionally, unbreaks one more case where setAudioOnly is incorrectly
dispatched on CONFERENCE_LEFT or CONFERENCE_FAILED and, consequently,
overrides the intent of the URL.
2018-04-16 22:02:37 -05:00
Lyubo Marinov
13e0e18f37 Coding style: formatting, typos 2018-04-16 18:09:08 -05:00
zbettenbuk
8c0bb377ba Calendar list shouldn't show unnormalised URIs 2018-04-16 18:09:08 -05:00
zbettenbuk
fc25125667 Fix app crash with special characters in the room name 2018-04-16 18:09:08 -05:00
virtuacoplenny
5b7b373e21 fix(keyboard-shortcuts): process Unidentified keys (#2813)
* fix(keyboard-shortcuts): process Unidentified keys

When processing keyboard events for keyboard shortcuts,
if the value of an event's "key" attribute is "Unidentified",
the event should be further processed instead of the "key"
being returned to be mapped to a registered keyboard shortcut.
This occurs on edge, where question mark has the key
"Unidentified" but has the proper keyCode of 191.

* squash: add comment
2018-04-16 16:33:26 -05:00
virtuacoplenny
8e42a7b034 fix(toolbar): make toolbar smaller (#2808) 2018-04-16 14:17:03 -07:00
virtuacoplenny
4bd94fc94c fix(invite): tweak invite modal copy and avatar sizes (#2818) 2018-04-16 13:58:20 -07:00
virtuacoplenny
41e1c3a2e2 fix(tooltip): description prop deprecated, use content instead (#2806) 2018-04-16 10:21:01 -07:00
zbettenbuk
6586be9a8e Fix plist file formatting 2018-04-15 23:16:44 -05:00
zbettenbuk
56d8210e35 Add ability to detect calendar permission description in the plist file (iOS) 2018-04-15 23:16:44 -05:00
Lyubo Marinov
f1ab160c62 Coding style: formatting, naming 2018-04-15 23:16:44 -05:00
Lyubo Marinov
eac74aa0b7 [RN] Fix _getRouteToRender after Deeplinking (#2760) 2018-04-15 23:16:44 -05:00
zbettenbuk
e30d141cec Proper use of getPropertyValue in base/media 2018-04-13 21:57:40 -05:00
zbettenbuk
1513e1f3b3 Make getPropertyValue's config easier to use 2018-04-13 21:57:07 -05:00
virtuacoplenny
0539e8f2df fix(recording): do not spell check stream key input (#2811) 2018-04-13 19:37:06 -07:00
hristoterezov
eb19f94598 Deeplinking (#2760)
* feat(Deeplinking): Implement for web.

* ref(unsupported_browser): Move the mobile version to deeplinking feature

* feat(deeplinking_mobile): Redesign.

* fix(deeplinking): Use interface.NATIVE_APP_NAME.

* feat(dial_in_summary): Add the PIN to the number link.

* fix(deep_linking): Handle use case when there isn't deep linking image.

* fix(deep_linking): css

* fix(deep_linking): deeplink -> "deep linking"

* fix(deeplinking_css): Remove position: fixed

* docs(deeplinking): Add comment for the openWebApp action.
2018-04-13 17:00:40 -07:00
Daniel Ornelas
fd44721bac Clean up PiP mode for iOS 2018-04-13 16:04:51 -05:00
virtuacoplenny
219b93a3c9 fix(recording): fetch events also for available broadcasts (#2810)
* fix(recording): fetch events also for available broadcasts

Only "persistent" broadcasts were being fetched using the
YouTube API. Fetching "all" will get persistent broadcasts
and events. If events use a custom encoder then the stream
key can be obtained. If google hangouts is used for the event
then a stream key will not be obtainable; in those cases
input empty string as the stream key.

* squash: fix typos, reword comments, use object for preventing duplicate broadcasts
2018-04-13 13:49:24 -07:00
virtuacoplenny
2a55548b84 fix(info): hide anchor hover colors for the call url (#2807)
The call url is an anchor element so that right clicking it
can bring up the copy link option in the context menu.
Clicking on it does a no-op so the anchor was colored to
look like plain text. Hovering over it right now makes it
look like an anchor due to some atlaskit color, so supress
the coloring.
2018-04-13 10:15:29 -07:00
paweldomas
8f142d5ec4 doc(testing/actions): fill missing in 'setConnectionState' 2018-04-13 11:35:25 -05:00
Leonard Kim
5cf16a20d3 ref(always-on-top): refactor to stop using old toolbar components 2018-04-13 10:09:04 +02:00
virtuacoplenny
be78ab5317 fix(welcome-page): clear update name timeout on unmount (#2800) 2018-04-12 14:23:30 -07:00
virtuacoplenny
907cb013a8 fix(hangup): ensure large video exists before getting displayed id (#2799)
On hangup while audio only, audio only is set to false on
conference leave to reset redux state on mobile. Large video will
update itself on conference leave, but large video has been cleaned
up by that time so trying to directly access the user ID on large
video will fail. Be defensive about this check, because its
callers are already defensive about its return value.
2018-04-12 14:23:03 -07:00
Leonard Kim
8828525511 chore(deps): update @atlaskit/tooltip to 9.1.1
Issues with tooltips not getting repositioned to display on
screen have been addressed.
2018-04-12 15:17:59 -05:00
paweldomas
c03e66954d fix(base/tracks): local track for video already exists
The _setMuted method in a corner case was making an attempt to create
second video track, because it was not taking pending tracks into
account.
2018-04-11 22:40:51 -05:00
virtuacoplenny
157800c494 fix(toolbar): video quality button shows current video quality (#2761) 2018-04-11 13:04:40 -07:00
virtuacoplenny
3285d647e6 feat(feedback): tweak styling (#2791)
- Green stars
- Label for feedback box
- Adjust margins/padding
2018-04-11 11:31:03 -07:00
Zoltan Bettenbuk
78ff0f7864 Separate handling config and profile with precedence (#2784) 2018-04-11 10:02:31 -07:00
Zoltan Bettenbuk
76c56339c4 Merge pull request #2794 from saghul/xcode-93
[iOS] Add new file created by Xcode 9.3
2018-04-11 12:29:43 +02:00
Saúl Ibarra Corretgé
95927c4804 [iOS] Add new file created by Xcode 9.3
Looks like it's a good idea to check it in: https://stackoverflow.com/a/49564624
2018-04-11 10:47:26 +02:00
Neil Brown
6c5482fb9d Update quick-install.md (#2657)
Add potential additional package to install, tested on fresh Debian 9 installation.
2018-04-11 10:42:27 +02:00
Leonard Kim
4f157b71f3 ref(toolbar): remove custom (old) InviteButton dropdown config 2018-04-11 10:35:01 +02:00
Leonard Kim
5270da4c14 ref(toolbar): remove reference to unused config autoEnableDesktopSharing 2018-04-11 10:35:01 +02:00
Leonard Kim
fe473bf426 ref(toolbar): remove old recording button logic 2018-04-11 10:35:01 +02:00
Leonard Kim
abee3331aa ref(toolbar): remove remnant of custom tooltip display on demand
The feature was not ported to the new toolbar. Arguable these
can all be moved into notification but for now simply the
logic will be removed and worked on again as demand arised.
2018-04-11 10:35:01 +02:00
Leonard Kim
a5e4fb000f ref(toolbar): removed unused dialpad logic
The old toolbar had a dialpad button that did a no-op.
Remove the remnant of that logic.
2018-04-11 10:35:01 +02:00
paweldomas
27a1be1e1c chore: update lib-jitsi-meet to 39eea17e7e2f8ff4cc273239a6e73f2a149b96e2 2018-04-10 16:43:12 -05:00
paweldomas
4d942440db feat: add TestConnectionInfo for mobile
Adds TestConnectionInfo component which exposes some internal app state
to the jitsi-meet-torture through the UI accessibility layer. This
component will render only if config.testing.testMode is set to true.
2018-04-10 16:43:12 -05:00
paweldomas
461540d874 ref(stats): start the statsEmitter for both mobile and web
Moves the statsEmitter.start() invocation to the middleware of
the connection-indicator feature, so that it's started for both mobile
and web (now mobile needs RTP stats for the tests).
2018-04-10 16:43:12 -05:00
Leonard Kim
3f99e80358 chore(deps): update Tooltip, InlineDialog, and InlineMessage 2018-04-10 16:42:46 -05:00
Leonard Kim
2ce3c2d459 ref(filmstirp): remove animate flag from resizeThumbnails
The flag is always false.
2018-04-10 16:18:45 -05:00
Leonard Kim
8363f3cfeb ref(toolbar): remove all jquery filmstrip animations
The animate flag is always being passed in as false, so
essentially the animation isn't needed, unless a setTimeout 0
behavior is for some reason required...
2018-04-10 16:18:45 -05:00
Leonard Kim
f8537dde6b fix(filmstrip): set SmallVideo styles instead of using animate
jquery animate during animations sets an element's overflow to
hidden and then back to the overflow declared before the start
of the animation. If multiple animations are fired, then the
overflow could be set to hidden permanently. No calls
to Filmstrip#resizeThumbnails have animate set to true, so the
animate call is not even needed.
2018-04-10 16:18:45 -05:00
virtuacoplenny
f34686afee fix(recording): return true to close stop recording modal on cancel (#2776) 2018-04-10 15:51:49 -05:00
virtuacoplenny
ea1aef0703 fix(toolbar): remove ref to removed MAIN_TOOLBAR_BUTTONS (#2787) 2018-04-10 15:51:37 -05:00
paweldomas
8b2ce21e1a fix(RN): bundle sound files in release build
On Android the files will be copied to the assets/sounds directory of
the SDK bundle on build time. To play the "asset:/" prefix has to be
used to locate the files correctly.

On iOS each sound file must be added to the SDK's Xcode project in order
to be bundled correctly. To playback we need to know the path of the SDK
bundle which is now exposed by the AppInfo iOS module.
2018-04-10 10:59:52 +02:00
Leonard Kim
c377219013 feat(info): invite url as an anchor for copying from context menu 2018-04-10 09:41:38 +02:00
Leonard Kim
10c8d380b7 fix(video-quality): different tooltips for different definitions 2018-04-10 09:39:37 +02:00
Leonard Kim
7e9a64d7c1 fix(quality-label): show eye icon when for muted video
Instead of continuing with HD/SD/LD, show the eye icon
for when the participant on large video is muted or has
no video.
2018-04-10 09:39:37 +02:00
Leonard Kim
a783939f12 ref(toolbar): remove old InviteButton 2018-04-10 09:34:52 +02:00
Leonard Kim
6883ee0141 ref(toolbar): rename ToolbarButtonV2 to ToolbarButton 2018-04-10 09:34:52 +02:00
Leonard Kim
1eee20dd5a ref(toolbar): remove contact list 2018-04-10 09:34:52 +02:00
Leonard Kim
7d86e3f8e7 ref(toolbar): remove Video Quality Button 2018-04-10 09:34:52 +02:00
Дамян Минков
60fde5efb8 deps: update node-sass to 4.8.3
Fixes #2762 #2237
2018-04-10 09:24:06 +02:00
Zoltan Bettenbuk
79b7e1641d Add pinch zoom functionality 2018-04-10 01:20:53 -05:00
Zoltan Bettenbuk
decbcefbd4 [RN] Don't press on Conference in preparation for 'pinch to zoom'
TouchableWithoutFeedback and TouchableHighlight interfere with the
implementation of 'pinch to zoom' to come. We prepare for it by driving
the onClick/onPress handler(s) out of Conference, through LargeVideo and
ParticipantView into Video itself where the bulk of 'pinch to zoom' will
be implemented.
2018-04-10 01:20:52 -05:00
Zoltan Bettenbuk
cb70c084b3 [RN] "The View is never the target of touch events"
In preparation for "pinch to zoom" support in desktop streams on mobile, make
certain Views not intervene in touch event handling. While the modification is
necessary for "pinch to zoom" which is coming later, it really makes sense for
the modified Views to not be involved in touching because they're used to aid
layout and/or animations and are to behave to the user as if they're not there.
2018-04-10 01:20:52 -05:00
virtuacoplenny
0afe72d42b fix(toolbar): tweak overflow menu order (#2781) 2018-04-09 23:06:01 -07:00
Daniel Ornelas
84e5e657a0 [iOS] Fix RNFetchBlob calling UI API on a background thread
Update RNFetchBlob to use a commit that fixes issues with calling UI API
on a background thread. Note: The commit used is from a forked repo that
is not yet merged on the new source for this RN component, eventually we
should be consuming from this repo instead
https://github.com/joltup/react-native-fetch-blob
2018-04-09 16:27:06 -05:00
virtuacoplenny
d8ad39ed3f fix(welcome-page): modify styling for narrow screens (#2724)
* fix(welcome-page): modify styling for narrow screens

* squash: fix autoscrolling on mobile safari
2018-04-09 15:50:57 -05:00
virtuacoplenny
16c2bc2d15 fix(invite): do not show caret in input (#2774) 2018-04-09 12:40:17 -07:00
Jacob MacElroy
01e0dfe58a Adding a prosody module to support sip-style call flows.
When combined with mod_muc_poltergeist mod_muc_call allows
for enabling call features using a proper ext_events.lib.lua
implementation. By default when the module is configured only
stub implementations are used for ext_events.lib.lua as these
are unique between deployments.
2018-04-09 13:46:17 -05:00
virtuacoplenny
38c8a41634 fix(toolbar): etherpad button should say open when etherpad is hidden (#2769) 2018-04-09 11:02:21 -07:00
Guus der Kinderen
b2efcadeb8 deps: update react-native-webrtc 2018-04-09 12:08:57 +02:00
Saúl Ibarra Corretgé
b73b51f1f4 feat(toolbox): axe the old toolbox (#2731)
This PR takes The Bulldozer Approach (R): removes the old toolbox and lots of
associated code, though not all of it. Subsequent cleanups will follow.
2018-04-08 22:03:26 -07:00
Leonard Kim
0cd32c8155 fix(filmstrip-only): override Atlaskit background for transparency 2018-04-06 15:18:05 -05:00
Leonard Kim
45adf3e26a fix(toolbar): adjust sizings and colors 2018-04-06 15:17:58 -05:00
Leonard Kim
58d1b69148 chore(deps): update lib-jitsi-meet to 7ab5434a 2018-04-06 15:17:50 -05:00
Leonard Kim
1f0dc6fcd8 fix(feedback): modify user-select none declaration for edge 2018-04-06 15:17:42 -05:00
Saúl Ibarra Corretgé
95e00405b6 feat(keyboard-shortcuts): fix removing shortcuts (#2749) 2018-04-06 08:11:21 -07:00
paweldomas
968b279b37 feat(android): support NAT64
Adds Nat64InfoModule which resolves IPv6 addresses for IPv4 addresses
in IPv6 only network where jitsi-meet deployment does not provide any
IPv6 addresses as ICE candidates.
2018-04-05 10:21:59 -05:00
Zoltan Bettenbuk
0456df239f Fix case sensitive recent list (#2730) 2018-04-04 12:54:42 -07:00
virtuacoplenny
2f23f8e400 Info dialog: bold labels, no url truncation, only auto show on lonely call (#2619)
* fix(info): bold info labels

* fix(info): do not truncate url

* feat(info): show only during a lonely call
2018-04-04 08:58:54 -07:00
Zoltan Bettenbuk
2412239206 [RN] Assorted hintbox fixes
* Align hint box text to center

* Fix disappearing hint box on android
2018-04-04 14:50:12 +02:00
Zoltan Bettenbuk
cc6e04ddf8 [RN] Fix calendar alerts when case sensitive URLs are used 2018-04-04 11:21:02 +02:00
paweldomas
6f11bbc400 feat(mobile): handle kicked out of the conference
Being kicked out of the conference will result with a conference failed
event with 'conference.kicked' reason and take the user back to
the welcome page by navigating to 'undefined'.
2018-04-03 16:02:15 -05:00
virtuacoplenny
7c08116dc2 ref(toolbar): add accessibility labels for torture tests (#2685)
* ref(toolbar): add accessibility labels for torture tests

* squash: update propTypes
2018-04-03 12:32:00 -07:00
Leonard Kim
f8717a7135 fix(gum): add electron string for requesting gum permissions
Electron generally can bypass having to get permission for
audio and video. In the case it doesn't have it, and the
permission screen is displayed, a string should still display
prompting the user to click allow. Right now the string id
displays.
2018-04-02 16:13:57 -05:00
Leonard Kim
1b85442dba chore(deps): update lib-jitsi-meet to 6282e7a8
This update is explicitly for FF screenshare and
gum retry fixes.
2018-04-02 16:13:35 -05:00
George Politis
ea46cbc479 feat: enableTcc, enableRemb, minParticipants. 2018-04-02 15:37:53 -05:00
Leonard Kim
b76ab305e3 fix(welcome-page): prevent form submit to prevent page refresh 2018-03-30 15:16:22 -05:00
Leonard Kim
358ce0799e fix(toolbar): the conference obj is needed to submit feedback 2018-03-30 14:45:27 -05:00
Leonard Kim
e7223c49ef fix(feedback): let star label color be inherited from atlaskit 2018-03-30 14:23:01 -05:00
Leonard Kim
02a31746fb fix(toolbar): do not use toggle class for recording 2018-03-30 13:07:38 -05:00
Leonard Kim
40154b1feb fix(toolbar): tweak chat backgrounds to be darker 2018-03-29 13:38:42 -05:00
Leonard Kim
4c49e3bec0 fix(toolbar): use old toolbar logic for showing screenshare
I don't understand the old showDesktopSharingButton action
but I've tried my best to copy it over. There is an existing
issue where the keyboard shortcut gets registered when it
probably shouldn't because screensharing is disabled. It will
be fixed soon with refactoring of the entire logic determining
whether or not to show the screensharing button.
2018-03-29 13:38:42 -05:00
Leonard Kim
0a086fa3f7 fix(toolbar): do not show invite button if features not available
This is instead of showing the button with a tooltip about it
being disabled.
2018-03-29 13:38:42 -05:00
virtuacoplenny
b353b8fffb chore(deps): update lib-jitsi-meet to ef0e14b (#2679) 2018-03-29 13:42:42 +02:00
Zoltan Bettenbuk
a3c00021de [RN] Fix showing user avatar in sidebar 2018-03-29 11:47:32 +02:00
virtuacoplenny
1e0a3ceb74 fix(chat): polyfills for lib-jitsi-meet ChatRoom#onPresence (#2678)
The onPresence parsing was refactored to remove use of jQuery.
This exposed three methods not available in react-native:
ParentNode.children, ChildNode.remove, and
document.querySelectorAll. The querySelectorAll change could
be swapped for the already polyfilled querySelector, but
children and remove had to be added. The polyfills are based
on those supplied by MDN web docs, but modified to pass jitsi
linting.
2018-03-28 18:04:42 -07:00
Boris Grozev
8492aad7d6 npm: Updates lib-jitsi-meet to cab2fabd56e9591148997c78a82da433ecf28dec. 2018-03-28 16:08:54 -05:00
Leonard Kim
7ad9fa8392 fix(toolbar): exercise old video muting flow
The redux flows for video muting may not cover all cases
that the conference.js flows cover. Just exercise the old
flows to be safe.
2018-03-28 13:52:04 -05:00
Leonard Kim
6916252ce1 fix(toolbar): exercise conference audio toggling
The redux flows do not account for as many scenarios, such
as config.startWithAudioMuted being true.
2018-03-28 13:52:04 -05:00
Leonard Kim
b4eae56eed fix(toolbar): css hacks to raise notifications 2018-03-28 13:52:04 -05:00
Saúl Ibarra Corretgé
eb69fb69cb feat(conference): lower the redirect timeout after feedback submission (#2673) 2018-03-28 07:35:26 -07:00
virtuacoplenny
2b7cdbc6a8 ref(toolbar): use new toolbar by default (#2666) 2018-03-27 19:39:42 -05:00
jitsi-pootle
f3a90f048a New files added from translate.jitsi.org based on templates 2018-03-27 23:38:51 +00:00
virtuacoplenny
8bf69d30b7 fix(toolbar): make darker for better text contrast (#2667)
* fix(toolbar): make darker for better text contrast

* squash: borrow some atlaskit colors
2018-03-27 15:30:04 -07:00
Saúl Ibarra Corretgé
45078fe6b2 [RN] Don't auto-correct any field in settings 2018-03-27 12:58:56 -07:00
Saúl Ibarra Corretgé
4783b22018 [RN] Don't auto-capitalize email field in settings 2018-03-27 12:58:56 -07:00
Leonard Kim
d93782af8a feat(new-toolbars): initial implementation 2018-03-27 00:54:30 -05:00
Shuai Li
962df14382 [iOS] Fix launching the app after the introduction of Swift in sdk 2018-03-26 23:14:45 -05:00
virtuacoplenny
01db70fd3d Merge pull request #2636 from zbettenbuk/calendar-permission-fix
Reorganize calendar access request flow
2018-03-26 07:57:24 -07:00
Дамян Минков
6cc8800016 Update poltergeist's presence with identity information. (#2650) 2018-03-23 13:58:05 -07:00
virtuacoplenny
e5596c3cd5 fix(recording): let the google app for api calls be overridable (#2653) 2018-03-23 10:52:32 -07:00
virtuacoplenny
1b91e0bc2f improve invite error handling (#2649)
* fix(invite): do not send empty queries for people search

The endpoint might return an error if an empty query is sent.

* fix(invite): add error logging for failed people directory requests

The error currently being passed through from $.getJSON ended up
being an empty string plus was not getting logged. So switch to
fetch to move along with jquery killing and log the error.

* fix(dial-in): add error logging for failed requests

* ref(invite): create a fetch helper to remove duplicate logic
2018-03-23 09:37:04 -07:00
zbettenbuk
b258e0d397 Reorganize calendar access request flow 2018-03-23 07:53:36 +01:00
virtuacoplenny
83f47c2df1 feat(invite): add basic analytics for AddPeople dialog (#2641)
* feat(invite): add basic analytics for AddPeople dialog

Analytics for opening the dialog, closing the dialog, the
count of invites sent, and the count of invites errored.

* squash: fix typo, change default count init, remove extra analytics param
2018-03-22 17:53:16 -07:00
virtuacoplenny
a39da15c94 ref(invite): remove unused nuclear-js dep (#2642) 2018-03-22 16:22:26 -07:00
Daniel Ornelas
fd787abf85 Change deployment target of JitsiMeet iOS SDK to 10.0 2018-03-22 16:40:46 -04:00
Daniel Ornelas
7822155e5e Fix iPad rotation related issue when in PiP 2018-03-22 16:40:17 -04:00
341 changed files with 14546 additions and 8116 deletions

View File

@@ -9,6 +9,7 @@ STYLES_BUNDLE = css/all.bundle.css
STYLES_DESTINATION = css/all.css
STYLES_MAIN = css/main.scss
WEBPACK = ./node_modules/.bin/webpack
WEBPACK_DEV_SERVER = ./node_modules/.bin/webpack-dev-server
all: compile deploy clean
@@ -56,6 +57,9 @@ deploy-css:
deploy-local:
([ ! -x deploy-local.sh ] || ./deploy-local.sh)
dev: deploy-init deploy-css deploy-lib-jitsi-meet
$(WEBPACK_DEV_SERVER)
source-package:
mkdir -p source_package/jitsi-meet/css && \
cp -r *.js *.html connection_optimization favicon.ico fonts images libs static sounds LICENSE lang source_package/jitsi-meet && \

View File

@@ -44,7 +44,7 @@ To build the Jitsi Meet application, just type
make
```
## Working with the library sources (lib-jitsi-meet)
### Working with the library sources (lib-jitsi-meet)
By default the library is build from its git repository sources. The default dependency path in package.json is :
```json
@@ -72,12 +72,12 @@ It allows to link `lib-jitsi-meet` dependency to local source in few steps:
```bash
cd lib-jitsi-meet
# create global symlink for lib-jitsi-meet package
#### create global symlink for lib-jitsi-meet package
npm link
cd ../jitsi-meet
# create symlink from the local node_modules folder to the global lib-jitsi-meet symlink
#### create symlink from the local node_modules folder to the global lib-jitsi-meet symlink
npm link lib-jitsi-meet
```
@@ -90,16 +90,17 @@ cd jitsi-meet
npm unlink lib-jitsi-meet
npm install
```
## Running with webpack-dev-server for development
### Running with webpack-dev-server for development
Use it at the CLI, type
```
node_modules/.bin/webpack-dev-server
make dev
```
By default the backend deployment used is `beta.meet.jit.si`, you can point the Jitsi-Meet app at a different backend by using a proxy server. To do this set the WEBPACK_DEV_SERVER_PROXY_TARGET variable, type
By default the backend deployment used is `beta.meet.jit.si`, you can point the Jitsi-Meet app at a different backend by using a proxy server. To do this set the WEBPACK_DEV_SERVER_PROXY_TARGET variable:
```
WEBPACK_DEV_SERVER_PROXY_TARGET=https://your-example-server.com node_modules/.bin/webpack-dev-server
export WEBPACK_DEV_SERVER_PROXY_TARGET=https://your-example-server.com
make dev
```
The app should be running at https://localhost:8080/
@@ -122,7 +123,7 @@ network but decrypted on the machine that hosts the bridge.
The Jitsi Meet architecture allows you to deploy your own version, including
all server components, and in that case your security guarantees will be roughly
equivalent to these of a direct one-to-one WebRTC call. This is what's unique to
equivalent to these of a direct one-to-one WebRTC call. This is what's unique to
Jitsi Meet in terms of security.
The [meet.jit.si](https://meet.jit.si) service is maintained by the Jitsi team

View File

@@ -19,12 +19,14 @@ package org.jitsi.meet;
import android.os.Bundle;
import android.util.Log;
import org.jitsi.meet.sdk.InviteSearchController;
import org.jitsi.meet.sdk.JitsiMeetActivity;
import org.jitsi.meet.sdk.JitsiMeetView;
import org.jitsi.meet.sdk.JitsiMeetViewListener;
import com.calendarevents.CalendarEventsPackage;
import java.util.HashMap;
import java.util.Map;
/**
@@ -84,6 +86,11 @@ public class MainActivity extends JitsiMeetActivity {
on("CONFERENCE_WILL_LEAVE", data);
}
@Override
public void launchNativeInvite(InviteSearchController inviteSearchController) {
on("LAUNCH_NATIVE_INVITE", new HashMap<String, Object>());
}
@Override
public void onLoadConfigError(Map<String, Object> data) {
on("LOAD_CONFIG_ERROR", data);

View File

@@ -33,6 +33,8 @@ dependencies {
compile project(':react-native-vector-icons')
compile project(':react-native-webrtc')
compile project(':react-native-calendar-events')
testCompile 'junit:junit:4.12'
}
// Build process helpers
@@ -69,6 +71,22 @@ gradle.projectsEvaluated {
runBefore("processUniversal${buildNameCapitalized}Resources", currentFontTask)
runBefore("process${buildNameCapitalized}Resources", currentFontTask)
def currentSoundsTask = tasks.create(
name: "copy${buildNameCapitalized}Sounds",
type: Copy) {
from("${projectDir}/../../sounds/joined.wav")
from("${projectDir}/../../sounds/left.wav")
into("${bundlePath}/assets/sounds")
}
currentSoundsTask.dependsOn("merge${buildNameCapitalized}Resources")
currentSoundsTask.dependsOn("merge${buildNameCapitalized}Assets")
runBefore("processArmeabi-v7a${buildNameCapitalized}Resources", currentSoundsTask)
runBefore("processX86${buildNameCapitalized}Resources", currentSoundsTask)
runBefore("processUniversal${buildNameCapitalized}Resources", currentSoundsTask)
runBefore("process${buildNameCapitalized}Resources", currentSoundsTask)
// Bundle JavaScript and React resources.
// (adapted from react-native/react.gradle)
//

View File

@@ -0,0 +1,184 @@
package org.jitsi.meet.sdk;
import android.util.Log;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.WritableArray;
import com.facebook.react.bridge.WritableNativeArray;
import com.facebook.react.bridge.WritableNativeMap;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
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.
*/
public class InviteSearchController {
/**
* The InviteSearchControllerDelegate for this controller, used to pass query
* results back to the native code that initiated the query.
*/
private InviteSearchControllerDelegate searchControllerDelegate;
/**
* Local cache of search query results. Used to re-hydrate the list
* of selected items based on their ids passed to submitSelectedItemIds
* in order to pass the full item maps back to the JitsiMeetView during submission.
*/
private Map<String, ReadableMap> items = new HashMap<>();
/**
* Randomly generated UUID, used for identification in the InviteSearchModule
*/
private String uuid = UUID.randomUUID().toString();
private WeakReference<InviteSearchModule> parentModuleRef;
public InviteSearchController(InviteSearchModule module) {
parentModuleRef = new WeakReference<>(module);
}
/**
* Start a search for entities to invite with the given query.
* Results will be returned through the associated InviteSearchControllerDelegate's
* onReceiveResults method.
*
* @param query
*/
public void performQuery(String query) {
JitsiMeetView.onInviteQuery(query, uuid);
}
/**
* Send invites to selected users based on their item ids
*
* @param ids
*/
public void submitSelectedItemIds(List<String> ids) {
WritableArray selectedItems = new WritableNativeArray();
for(int i=0; i<ids.size(); i++) {
if(items.containsKey(ids.get(i))) {
WritableNativeMap map = new WritableNativeMap();
map.merge(items.get(ids.get(i)));
selectedItems.pushMap(map);
} else {
// if the id doesn't exist in the map, we can't do anything, so just skip it
}
}
JitsiMeetView.submitSelectedItems(selectedItems, uuid);
}
/**
* 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
*/
void receivedResultsForQuery(ReadableArray results, String query) {
List<Map<String, Object>> jvmResults = new ArrayList<>();
// cache results for use in submission later
// convert to jvm array
for(int i=0; i<results.size(); i++) {
ReadableMap map = results.getMap(i);
if(map.hasKey("id")) {
items.put(map.getString("id"), map);
} else if(map.hasKey("type") && map.getString("type").equals("phone") && map.hasKey("number")) {
items.put(map.getString("number"), map);
} else {
Log.w("InviteSearchController", "Received result without id and that was not a phone number, so not adding it to suggestions: " + map);
}
jvmResults.add(map.toHashMap());
}
searchControllerDelegate.onReceiveResults(this, jvmResults, query);
}
/**
*
* @return the InviteSearchControllerDelegate for this controller, used to pass query
* results back to the native code that initiated the query.
*/
public InviteSearchControllerDelegate getSearchControllerDelegate() {
return searchControllerDelegate;
}
/**
* Sets the InviteSearchControllerDelegate for this controller, used to pass query results
* back to the native code that initiated the query.
*
* @param searchControllerDelegate
*/
public void setSearchControllerDelegate(InviteSearchControllerDelegate searchControllerDelegate) {
this.searchControllerDelegate = searchControllerDelegate;
}
/**
* Cancel the invitation flow and free memory allocated to the InviteSearchController. After
* calling this method, this object is invalid - a new InviteSearchController will be passed
* to the caller through launchNativeInvite.
*/
public void cancelSearch() {
InviteSearchModule parentModule = parentModuleRef.get();
if(parentModule != null) {
parentModule.removeSearchController(uuid);
}
}
/**
* @return the unique identifier for this InviteSearchController
*/
public String getUuid() {
return uuid;
}
public interface InviteSearchControllerDelegate {
/**
* Called when results are received for a query called through InviteSearchController.query()
*
* @param searchController
* @param results a List of Map<String, Object> objects that represent items returned by the query.
* The object at key "type" describes the type of item: "user", "videosipgw" (conference room), or "phone".
* "user" types have properties at "id", "name", and "avatar"
* "videosipgw" types have properties at "id" and "name"
* "phone" types have properties at "number", "title", "and "subtitle"
* @param query the query that generated the given results
*/
void onReceiveResults(InviteSearchController searchController, List<Map<String, Object>> results, String query);
/**
* Called when the call to {@link InviteSearchController#submitSelectedItemIds(List)} completes successfully
* and invitations are sent to all given IDs.
*
* @param searchController the active {@link InviteSearchController} for this invite flow. This object will be
* cleaned up after the call to inviteSucceeded completes.
*/
void inviteSucceeded(InviteSearchController searchController);
/**
* Called when the call to {@link InviteSearchController#submitSelectedItemIds(List)} completes, but the
* invitation fails for one or more of the selected items.
*
* @param searchController the active {@link InviteSearchController} for this invite flow. This object
* should be cleaned up by calling {@link InviteSearchController#cancelSearch()} if
* the user exits the invite flow. Otherwise, it can stay active if the user
* will attempt to invite
* @param failedInviteItems a {@code List} of {@code Map<String, Object>} dictionaries that represent the
* invitations that failed. The data type of the objects is identical to the results
* returned in onReceiveResuls.
*/
void inviteFailed(InviteSearchController searchController, List<Map<String, Object>> failedInviteItems);
}
}

View File

@@ -0,0 +1,126 @@
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.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
/**
* Native module for Invite Search
*/
class InviteSearchModule extends ReactContextBaseJavaModule {
/**
* Map of InviteSearchController objects passed to connected JitsiMeetView.
* A call to launchNativeInvite will create a new InviteSearchController and pass
* it back to the caller. On a successful invitation, the controller will be removed automatically.
* On a failed invitation, the caller has the option of calling InviteSearchController#cancelSearch()
* to remove the controller from this map. The controller should also be removed if the user cancels
* the invitation flow.
*/
private Map<String, InviteSearchController> searchControllers = new HashMap<>();
public InviteSearchModule(ReactApplicationContext reactContext) {
super(reactContext);
}
/**
* Launch the native user invite flow
*
* @param externalAPIScope a string that represents a connection to a specific JitsiMeetView
*/
@ReactMethod
public void launchNativeInvite(String externalAPIScope) {
JitsiMeetView viewToLaunchInvite = JitsiMeetView.findViewByExternalAPIScope(externalAPIScope);
if(viewToLaunchInvite == null) {
return;
}
if(viewToLaunchInvite.getListener() == null) {
return;
}
InviteSearchController controller = createSearchController();
viewToLaunchInvite.getListener().launchNativeInvite(controller);
}
/**
* Callback for results received from the JavaScript invite search call
*
* @param results the results in a ReadableArray of ReadableMap objects
* @param query the query associated with the search
* @param inviteSearchControllerScope a string that represents a connection to a specific InviteSearchController
*/
@ReactMethod
public void receivedResults(ReadableArray results, String query, String inviteSearchControllerScope) {
InviteSearchController controller = searchControllers.get(inviteSearchControllerScope);
if(controller == null) {
Log.w("InviteSearchModule", "Received results, but unable to find active controller to send results back");
return;
}
controller.receivedResultsForQuery(results, query);
}
/**
* Callback for invitation failures
*
* @param items the items for which the invitation failed
* @param inviteSearchControllerScope a string that represents a connection to a specific InviteSearchController
*/
@ReactMethod
public void inviteFailedForItems(ReadableArray items, String inviteSearchControllerScope) {
InviteSearchController controller = searchControllers.get(inviteSearchControllerScope);
if(controller == null) {
Log.w("InviteSearchModule", "Invite failed, but unable to find active controller to notify");
return;
}
ArrayList<Map<String, Object>> jvmItems = new ArrayList<>();
for(int i=0; i<items.size(); i++) {
ReadableMap item = items.getMap(i);
jvmItems.add(item.toHashMap());
}
controller.getSearchControllerDelegate().inviteFailed(controller, jvmItems);
}
@ReactMethod
public void inviteSucceeded(String inviteSearchControllerScope) {
InviteSearchController controller = searchControllers.get(inviteSearchControllerScope);
if(controller == null) {
Log.w("InviteSearchModule", "Invite succeeded, but unable to find active controller to notify");
return;
}
controller.getSearchControllerDelegate().inviteSucceeded(controller);
searchControllers.remove(inviteSearchControllerScope);
}
void removeSearchController(String inviteSearchControllerUuid) {
searchControllers.remove(inviteSearchControllerUuid);
}
@Override
public String getName() {
return "InviteSearch";
}
private InviteSearchController createSearchController() {
InviteSearchController searchController = new InviteSearchController(this);
searchControllers.put(searchController.getUuid(), searchController);
return searchController;
}
}

View File

@@ -21,7 +21,6 @@ import android.app.Application;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
@@ -33,7 +32,9 @@ import com.facebook.react.ReactRootView;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.WritableArray;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.bridge.WritableNativeMap;
import com.facebook.react.common.LifecycleState;
import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler;
import com.facebook.react.modules.core.DeviceEventManagerModule;
@@ -43,6 +44,7 @@ import java.net.URL;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.WeakHashMap;
@@ -76,9 +78,11 @@ public class JitsiMeetView extends FrameLayout {
new AppInfoModule(reactContext),
new AudioModeModule(reactContext),
new ExternalAPIModule(reactContext),
new InviteSearchModule(reactContext),
new PictureInPictureModule(reactContext),
new ProximityModule(reactContext),
new WiFiStatsModule(reactContext)
new WiFiStatsModule(reactContext),
new org.jitsi.meet.sdk.net.NAT64AddrInfoModule(reactContext)
);
}
@@ -268,15 +272,43 @@ public class JitsiMeetView extends FrameLayout {
sendEvent("onUserLeaveHint", null);
}
/**
* Starts a query for users to invite to the conference. Results will be
* returned through the {@link InviteSearchController.InviteSearchControllerDelegate#onReceiveResults(InviteSearchController, List, String)}
* method.
*
* @param query {@code String} to use for the query
*/
public static void onInviteQuery(String query, String inviteSearchControllerScope) {
WritableNativeMap params = new WritableNativeMap();
params.putString("query", query);
params.putString("inviteScope", inviteSearchControllerScope);
sendEvent("performQueryAction", params);
}
/**
* Sends JavaScript event to submit invitations to the given item ids
*
* @param selectedItems a WritableArray of WritableNativeMaps representing selected items.
* Each map representing a selected item should match the data passed
* back in the return from a query.
*/
public static void submitSelectedItems(WritableArray selectedItems, String inviteSearchControllerScope) {
WritableNativeMap params = new WritableNativeMap();
params.putArray("selectedItems", selectedItems);
params.putString("inviteScope", inviteSearchControllerScope);
sendEvent("performSubmitInviteAction", params);
}
/**
* Helper function to send an event to JavaScript.
*
* @param eventName {@code String} containing the event name.
* @param params {@code WritableMap} optional ancillary data for the event.
* @param data {@code Object} optional ancillary data for the event.
*/
private static void sendEvent(
String eventName,
@Nullable WritableMap params) {
@Nullable Object data) {
if (reactInstanceManager != null) {
ReactContext reactContext
= reactInstanceManager.getCurrentReactContext();
@@ -284,11 +316,16 @@ public class JitsiMeetView extends FrameLayout {
reactContext
.getJSModule(
DeviceEventManagerModule.RCTDeviceEventEmitter.class)
.emit(eventName, params);
.emit(eventName, data);
}
}
}
/**
* Whether user invitation is enabled.
*/
private boolean addPeopleEnabled;
/**
* The default base {@code URL} used to join a conference when a partial URL
* (e.g. a room name only) is specified to {@link #loadURLString(String)} or
@@ -296,6 +333,11 @@ public class JitsiMeetView extends FrameLayout {
*/
private URL defaultURL;
/**
* Whether the ability to add users by phone number is enabled.
*/
private boolean dialOutEnabled;
/**
* The unique identifier of this {@code JitsiMeetView} within the process
* for the purposes of {@link ExternalAPI}. The name scope was inspired by
@@ -454,6 +496,9 @@ public class JitsiMeetView extends FrameLayout {
// welcomePageEnabled
props.putBoolean("welcomePageEnabled", welcomePageEnabled);
props.putBoolean("addPeopleEnabled", addPeopleEnabled);
props.putBoolean("dialOutEnabled", dialOutEnabled);
// XXX The method loadURLObject: is supposed to be imperative i.e.
// a second invocation with one and the same URL is expected to join
// the respective conference again if the first invocation was followed
@@ -535,6 +580,18 @@ public class JitsiMeetView extends FrameLayout {
}
}
/**
* Sets whether the ability to add users to the call is enabled.
* If this is enabled, an add user button will appear on the {@link JitsiMeetView}.
* If enabled, and the user taps the add user button,
* {@link JitsiMeetViewListener#launchNativeInvite(Map)} will be called.
*
* @param addPeopleEnabled {@code true} to enable the add people button; otherwise, {@code false}
*/
public void setAddPeopleEnabled(boolean addPeopleEnabled) {
this.addPeopleEnabled = addPeopleEnabled;
}
/**
* Sets the default base {@code URL} used to join a conference when a
* partial URL (e.g. a room name only) is specified to
@@ -548,6 +605,18 @@ public class JitsiMeetView extends FrameLayout {
this.defaultURL = defaultURL;
}
/**
* Sets whether the ability to add phone numbers to the call is enabled.
* Must be enabled along with {@link #setAddPeopleEnabled(boolean)} to
* be effective.
*
* @param dialOutEnabled {@code true} to enable the ability to add
* phone numbers to the call; otherwise, {@code false}
*/
public void setDialOutEnabled(boolean dialOutEnabled) {
this.dialOutEnabled = dialOutEnabled;
}
/**
* Sets a specific {@link JitsiMeetViewListener} on this
* {@code JitsiMeetView}.

View File

@@ -46,4 +46,8 @@ public abstract class JitsiMeetViewAdapter implements JitsiMeetViewListener {
@Override
public void onLoadConfigError(Map<String, Object> data) {
}
@Override
public void launchNativeInvite(InviteSearchController inviteSearchController) {
}
}

View File

@@ -59,6 +59,16 @@ public interface JitsiMeetViewListener {
*/
void onConferenceWillLeave(Map<String, Object> data);
/**
* Called when the add user button is tapped.
*
* @param inviteSearchController {@code InviteSearchController} scoped
* for this user invite flow. The {@code InviteSearchController} is used
* to start user queries and accepts an {@code InviteSearchControllerDelegate}
* for receiving user query responses.
*/
void launchNativeInvite(InviteSearchController inviteSearchController);
/**
* Called when loading the main configuration file from the Jitsi Meet
* deployment fails.

View File

@@ -44,14 +44,26 @@ public class PictureInPictureModule extends ReactContextBaseJavaModule {
PictureInPictureParams.Builder builder
= new PictureInPictureParams.Builder()
.setAspectRatio(new Rational(1, 1));
boolean r
= currentActivity.enterPictureInPictureMode(builder.build());
Throwable error;
if (r) {
// https://developer.android.com/reference/android/app/Activity.html#enterPictureInPictureMode(android.app.PictureInPictureParams)
//
// The system may disallow entering picture-in-picture in various
// cases, including when the activity is not visible, if the screen
// is locked or if the user has an activity pinned.
try {
error
= currentActivity.enterPictureInPictureMode(builder.build())
? null
: new Exception("Failed to enter Picture-in-Picture");
} catch (RuntimeException re) {
error = re;
}
if (error == null) {
promise.resolve(null);
} else {
promise.reject(
new Exception("Failed to enter Picture-in-Picture"));
promise.reject(error);
}
return;

View File

@@ -0,0 +1,238 @@
/*
* 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.net;
import java.net.InetAddress;
import java.net.UnknownHostException;
/**
* Constructs IPv6 addresses for IPv4 addresses in the NAT64 environment.
*
* NAT64 translates IPv4 to IPv6 addresses by adding "well known" prefix and
* suffix configured by the administrator. Those are figured out by discovering
* both IPv6 and IPv4 addresses of a host and then trying to find a place where
* the IPv4 address fits into the format described here:
* https://tools.ietf.org/html/rfc6052#section-2.2
*/
public class NAT64AddrInfo {
/**
* Coverts bytes array to upper case HEX string.
*
* @param bytes an array of bytes to be converted
* @return ex. "010AFF" for an array of {1, 10, 255}.
*/
static String bytesToHexString(byte[] bytes) {
StringBuilder hexStr = new StringBuilder();
for (byte b : bytes) {
hexStr.append(String.format("%02X", b));
}
return hexStr.toString();
}
/**
* Tries to discover the NAT64 prefix/suffix based on the IPv4 and IPv6
* addresses resolved for given {@code host}.
*
* @param host the host for which the code will try to discover IPv4 and
* IPv6 addresses which then will be used to figure out the NAT64 prefix.
* @return {@link NAT64AddrInfo} instance if the NAT64 prefix/suffix was
* successfully discovered or {@code null} if it failed for any reason.
* @throws UnknownHostException thrown by {@link InetAddress#getAllByName}.
*/
public static NAT64AddrInfo discover(String host)
throws UnknownHostException {
InetAddress ipv4 = null;
InetAddress ipv6 = null;
for(InetAddress addr : InetAddress.getAllByName(host)) {
byte[] bytes = addr.getAddress();
if (bytes.length == 4) {
ipv4 = addr;
} else if (bytes.length == 16) {
ipv6 = addr;
}
}
if (ipv4 != null && ipv6 != null) {
return figureOutNAT64AddrInfo(ipv4.getAddress(), ipv6.getAddress());
}
return null;
}
/**
* Based on IPv4 and IPv6 addresses of the same host, the method will make
* an attempt to figure out what are the NAT64 prefix and suffix.
*
* @param ipv4AddrBytes the IPv4 address of the same host in NAT64 network,
* as returned by {@link InetAddress#getAddress()}.
* @param ipv6AddrBytes the IPv6 address of the same host in NAT64 network,
* as returned by {@link InetAddress#getAddress()}.
* @return {@link NAT64AddrInfo} instance which contains the prefix/suffix
* of the current NAT64 network or {@code null} if the prefix could not be
* found.
*/
static NAT64AddrInfo figureOutNAT64AddrInfo(
byte[] ipv4AddrBytes,
byte[] ipv6AddrBytes) {
String ipv6Str = bytesToHexString(ipv6AddrBytes);
String ipv4Str = bytesToHexString(ipv4AddrBytes);
// NAT64 address format:
// +--+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
// |PL| 0-------------32--40--48--56--64--72--80--88--96--104---------|
// +--+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
// |32| prefix |v4(32) | u | suffix |
// +--+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
// |40| prefix |v4(24) | u |(8)| suffix |
// +--+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
// |48| prefix |v4(16) | u | (16) | suffix |
// +--+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
// |56| prefix |(8)| u | v4(24) | suffix |
// +--+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
// |64| prefix | u | v4(32) | suffix |
// +--+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
// |96| prefix | v4(32) |
// +--+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
int prefixLength = 96;
int suffixLength = 0;
String prefix = null;
String suffix = null;
if (ipv4Str.equalsIgnoreCase(ipv6Str.substring(prefixLength / 4))) {
prefix = ipv6Str.substring(0, prefixLength / 4);
} else {
// Cut out the 'u' octet
ipv6Str = ipv6Str.substring(0, 16) + ipv6Str.substring(18);
for (prefixLength = 64, suffixLength = 6; prefixLength >= 32; ) {
if (ipv4Str.equalsIgnoreCase(
ipv6Str.substring(
prefixLength / 4, prefixLength / 4 + 8))) {
prefix = ipv6Str.substring(0, prefixLength / 4);
suffix = ipv6Str.substring(ipv6Str.length() - suffixLength);
break;
}
prefixLength -= 8;
suffixLength += 2;
}
}
return prefix != null ? new NAT64AddrInfo(prefix, suffix) : null;
}
/**
* An overload for {@link #hexStringToIPv6String(StringBuilder)}.
*
* @param hexStr a hex representation of IPv6 address bytes.
* @return an IPv6 address string.
*/
static String hexStringToIPv6String(String hexStr) {
return hexStringToIPv6String(new StringBuilder(hexStr));
}
/**
* Converts from HEX representation of IPv6 address bytes into IPv6 address
* string which includes the ':' signs.
*
* @param str a hex representation of IPv6 address bytes.
* @return eg. FE80:CD00:0000:0CDA:1357:0000:212F:749C
*/
static String hexStringToIPv6String(StringBuilder str) {
for (int i = 32 - 4; i > 0; i -= 4) {
str.insert(i, ":");
}
return str.toString().toUpperCase();
}
/**
* Parses an IPv4 address string and returns it's byte array representation.
*
* @param ipv4Address eg. '192.168.3.23'
* @return byte representation of given IPv4 address string.
* @throws IllegalArgumentException if the address is not in valid format.
*/
static byte[] ipv4AddressStringToBytes(String ipv4Address) {
InetAddress address;
try {
address = InetAddress.getByName(ipv4Address);
} catch (UnknownHostException e) {
throw new IllegalArgumentException(
"Invalid IP address: " + ipv4Address, e);
}
byte[] bytes = address.getAddress();
if (bytes.length != 4) {
throw new IllegalArgumentException(
"Not an IPv4 address: " + ipv4Address);
}
return bytes;
}
/**
* The NAT64 prefix added to construct IPv6 from an IPv4 address.
*/
private final String prefix;
/**
* The NAT64 suffix (if any) used to construct IPv6 from an IPv4 address.
*/
private final String suffix;
/**
* Creates new instance of {@link NAT64AddrInfo}.
*
* @param prefix the NAT64 prefix.
* @param suffix the NAT64 suffix.
*/
private NAT64AddrInfo(String prefix, String suffix) {
this.prefix = prefix;
this.suffix = suffix;
}
/**
* Based on the NAT64 prefix and suffix will create an IPv6 representation
* of the given IPv4 address.
*
* @param ipv4Address eg. '192.34.2.3'
* @return IPv6 address string eg. FE80:CD00:0000:0CDA:1357:0000:212F:749C
* @throws IllegalArgumentException if given string is not a valid IPv4
* address.
*/
public String getIPv6Address(String ipv4Address) {
byte[] ipv4AddressBytes = ipv4AddressStringToBytes(ipv4Address);
StringBuilder newIPv6Str = new StringBuilder();
newIPv6Str.append(prefix);
newIPv6Str.append(bytesToHexString(ipv4AddressBytes));
if (suffix != null) {
// Insert the 'u' octet.
newIPv6Str.insert(16, "00");
newIPv6Str.append(suffix);
}
return hexStringToIPv6String(newIPv6Str);
}
}

View File

@@ -0,0 +1,122 @@
/*
* 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.net;
import android.util.Log;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import java.net.UnknownHostException;
/**
* This module exposes the functionality of creating an IPv6 representation
* of IPv4 addresses in NAT64 environment.
*
* See[1] and [2] for more info on what NAT64 is.
* [1]: https://tools.ietf.org/html/rfc6146
* [2]: https://tools.ietf.org/html/rfc6052
*/
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.
*/
private final static String HOST = "nat64.jitsi.net";
/**
* How long is the {@link NAT64AddrInfo} instance valid.
*/
private final static long INFO_LIFETIME = 60 * 1000;
/**
* The name of this module.
*/
private final static String MODULE_NAME = "NAT64AddrInfo";
/**
* The {@code Log} tag {@code NAT64AddrInfoModule} is to log messages with.
*/
private final static String TAG = MODULE_NAME;
/**
* The {@link NAT64AddrInfo} instance which holds NAT64 prefix/suffix.
*/
private NAT64AddrInfo info;
/**
* When {@link #info} was created.
*/
private long infoTimestamp;
/**
* Creates new {@link NAT64AddrInfoModule}.
*
* @param reactContext the react context to be used by the new module
* instance.
*/
public NAT64AddrInfoModule(ReactApplicationContext reactContext) {
super(reactContext);
}
/**
* Tries to obtain IPv6 address for given IPv4 address in NAT64 environment.
*
* @param ipv4Address IPv4 address string.
* @param promise a {@link Promise} which will be resolved either with IPv6
* address for given IPv4 address or with {@code null} if no
* {@link NAT64AddrInfo} was resolved for the current network. Will be
* rejected if given {@code ipv4Address} is not a valid IPv4 address.
*/
@ReactMethod
public void getIPv6Address(String ipv4Address, final Promise promise) {
// Reset if cached for too long.
if (System.currentTimeMillis() - infoTimestamp > INFO_LIFETIME) {
info = null;
}
if (info == null) {
String host = HOST;
try {
info = NAT64AddrInfo.discover(host);
} catch (UnknownHostException e) {
Log.e(TAG, "NAT64AddrInfo.discover: " + host, e);
}
infoTimestamp = System.currentTimeMillis();
}
String result;
try {
result = info == null ? null : info.getIPv6Address(ipv4Address);
} catch (IllegalArgumentException exc) {
Log.e(TAG, "Failed to get IPv6 address for: " + ipv4Address, exc);
// We don't want to reject. It's not a big deal if there's no IPv6
// address resolved.
result = null;
}
promise.resolve(result);
}
@Override
public String getName() {
return MODULE_NAME;
}
}

View File

@@ -0,0 +1,150 @@
/*
* 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.net;
import org.junit.Test;
import java.math.BigInteger;
import java.net.UnknownHostException;
import static org.junit.Assert.*;
/**
* Tests for {@link NAT64AddrInfo} class.
*/
public class NAT64AddrInfoTest {
/**
* Test case for the 96 prefix length.
*/
@Test
public void test96Prefix() {
testPrefixSuffix(
"260777000000000400000000", "", "203.0.113.1", "23.17.23.3");
}
/**
* Test case for the 64 prefix length.
*/
@Test
public void test64Prefix() {
String prefix = "1FF2A227B3AAF3D2";
String suffix = "BB87C8";
testPrefixSuffix(prefix, suffix, "48.46.87.34", "23.87.145.4");
}
/**
* Test case for the 56 prefix length.
*/
@Test
public void test56Prefix() {
String prefix = "1FF2A227B3AAF3";
String suffix = "A2BB87C8";
testPrefixSuffix(prefix, suffix, "34.72.234.255", "1.235.3.65");
}
/**
* Test case for the 48 prefix length.
*/
@Test
public void test48Prefix() {
String prefix = "1FF2A227B3AA";
String suffix = "72A2BB87C8";
testPrefixSuffix(prefix, suffix, "97.54.3.23", "77.49.0.33");
}
/**
* Test case for the 40 prefix length.
*/
@Test
public void test40Prefix() {
String prefix = "1FF2A227B3";
String suffix = "D972A2BB87C8";
testPrefixSuffix(prefix, suffix, "10.23.56.121", "97.65.32.21");
}
/**
* Test case for the 32 prefix length.
*/
@Test
public void test32Prefix()
throws UnknownHostException {
String prefix = "1FF2A227";
String suffix = "20D972A2BB87C8";
testPrefixSuffix(prefix, suffix, "162.63.65.189", "135.222.84.206");
}
private static String buildIPv6Addr(
String prefix, String suffix, String ipv4Hex) {
String ipv6Str = prefix + ipv4Hex + suffix;
if (suffix.length() > 0) {
ipv6Str = new StringBuilder(ipv6Str).insert(16, "00").toString();
}
return ipv6Str;
}
private void testPrefixSuffix(
String prefix, String suffix, String ipv4, String otherIPv4) {
byte[] ipv4Bytes = NAT64AddrInfo.ipv4AddressStringToBytes(ipv4);
String ipv4String = NAT64AddrInfo.bytesToHexString(ipv4Bytes);
String ipv6Str = buildIPv6Addr(prefix, suffix, ipv4String);
BigInteger ipv6Address = new BigInteger(ipv6Str, 16);
NAT64AddrInfo nat64AddrInfo
= NAT64AddrInfo.figureOutNAT64AddrInfo(
ipv4Bytes, ipv6Address.toByteArray());
assertNotNull("Failed to figure out NAT64 info", nat64AddrInfo);
String newIPv6 = nat64AddrInfo.getIPv6Address(ipv4);
assertEquals(
NAT64AddrInfo.hexStringToIPv6String(ipv6Address.toString(16)),
newIPv6);
byte[] ipv4Addr2 = NAT64AddrInfo.ipv4AddressStringToBytes(otherIPv4);
String ipv4Addr2Hex = NAT64AddrInfo.bytesToHexString(ipv4Addr2);
newIPv6 = nat64AddrInfo.getIPv6Address(otherIPv4);
assertEquals(
NAT64AddrInfo.hexStringToIPv6String(
buildIPv6Addr(prefix, suffix, ipv4Addr2Hex)),
newIPv6);
}
@Test
public void testInvalidIPv4Format() {
testInvalidIPv4Format("256.1.2.3");
testInvalidIPv4Format("FE80:CD00:0000:0CDA:1357:0000:212F:749C");
}
private void testInvalidIPv4Format(String ipv4Str) {
try {
NAT64AddrInfo.ipv4AddressStringToBytes(ipv4Str);
fail("Did not throw IllegalArgumentException");
} catch (IllegalArgumentException exc) {
/* OK */
}
}
}

View File

@@ -21,13 +21,13 @@ import {
createSelectParticipantFailedEvent,
createStreamSwitchDelayEvent,
createTrackMutedEvent,
initAnalytics,
sendAnalytics
} from './react/features/analytics';
import {
redirectWithStoredParams,
reloadWithStoredParams
} from './react/features/app';
import { updateRecordingState } from './react/features/recording';
import EventEmitter from 'events';
@@ -43,11 +43,11 @@ import {
lockStateChanged,
onStartMutedPolicyChanged,
p2pStatusChanged,
sendLocalParticipant
sendLocalParticipant,
setDesktopSharingEnabled
} from './react/features/base/conference';
import { updateDeviceList } from './react/features/base/devices';
import {
isAnalyticsEnabled,
isFatalJitsiConnectionError,
JitsiConferenceErrors,
JitsiConferenceEvents,
@@ -93,7 +93,6 @@ import {
getLocationContextRoot,
getJitsiMeetGlobalNS
} from './react/features/base/util';
import { statsEmitter } from './react/features/connection-indicator';
import { showDesktopPicker } from './react/features/desktop-picker';
import { appendSuffix } from './react/features/display-name';
import {
@@ -104,10 +103,8 @@ import {
mediaPermissionPromptVisibilityChanged,
suspendDetected
} from './react/features/overlay';
import {
isButtonEnabled,
showDesktopSharingButton
} from './react/features/toolbox';
import { setSharedVideoStatus } from './react/features/shared-video';
import { isButtonEnabled } from './react/features/toolbox';
const logger = require('jitsi-meet-logger').getLogger(__filename);
@@ -234,13 +231,14 @@ function maybeRedirectToWelcomePage(options) {
}));
}
// if Welcome page is enabled redirect to welcome page after 3 sec.
// if Welcome page is enabled redirect to welcome page after 3 sec, if
// there is a thank you message to be shown, 0.5s otherwise.
if (config.enableWelcomePage) {
setTimeout(
() => {
APP.store.dispatch(redirectWithStoredParams('/'));
},
3000);
options.showThankYou ? 3000 : 500);
}
}
@@ -505,16 +503,6 @@ export default {
*/
desktopSharingDisabledTooltip: null,
/*
* Whether the local "raisedHand" flag is on.
*/
isHandRaised: false,
/*
* Whether the local participant is the dominant speaker in the conference.
*/
isDominantSpeaker: false,
/**
* The local audio track (if any).
* FIXME tracks from redux store should be the single source of truth
@@ -695,53 +683,20 @@ export default {
/**
* Open new connection and join to the conference.
* @param {object} options
* @param {string} roomName name of the conference
* @param {string} roomName - The name of the conference.
* @returns {Promise}
*/
init(options) {
this.roomName = options.roomName;
// attaches global error handler, if there is already one, respect it
if (JitsiMeetJS.getGlobalOnErrorHandler) {
const oldOnErrorHandler = window.onerror;
// eslint-disable-next-line max-params
window.onerror = (message, source, lineno, colno, error) => {
JitsiMeetJS.getGlobalOnErrorHandler(
message, source, lineno, colno, error);
if (oldOnErrorHandler) {
oldOnErrorHandler(message, source, lineno, colno, error);
}
};
const oldOnUnhandledRejection = window.onunhandledrejection;
window.onunhandledrejection = function(event) {
JitsiMeetJS.getGlobalOnErrorHandler(
null, null, null, null, event.reason);
if (oldOnUnhandledRejection) {
oldOnUnhandledRejection(event);
}
};
}
return (
JitsiMeetJS.init({
enableAnalyticsLogging: isAnalyticsEnabled(APP.store),
...config
}).then(() => {
initAnalytics(APP.store);
return this.createInitialLocalTracksAndConnect(
options.roomName, {
startAudioOnly: config.startAudioOnly,
startScreenSharing: config.startScreenSharing,
startWithAudioMuted: config.startWithAudioMuted,
startWithVideoMuted: config.startWithVideoMuted
});
})
this.createInitialLocalTracksAndConnect(
options.roomName, {
startAudioOnly: config.startAudioOnly,
startScreenSharing: config.startScreenSharing,
startWithAudioMuted: config.startWithAudioMuted,
startWithVideoMuted: config.startWithVideoMuted
})
.then(([ tracks, con ]) => {
tracks.forEach(track => {
if ((track.isAudioTrack() && this.isLocalAudioMuted())
@@ -773,7 +728,8 @@ export default {
JitsiMeetConferenceEvents.DESKTOP_SHARING_ENABLED_CHANGED,
this.isDesktopSharingEnabled);
APP.store.dispatch(showDesktopSharingButton());
APP.store.dispatch(
setDesktopSharingEnabled(this.isDesktopSharingEnabled));
this._createRoom(tracks);
APP.remoteControl.init();
@@ -1364,7 +1320,6 @@ export default {
this.isSharingScreen = newStream && newStream.videoType === 'desktop';
if (wasSharingScreen !== this.isSharingScreen) {
APP.UI.updateDesktopSharingButtons();
APP.API.notifyScreenSharingStatusChanged(this.isSharingScreen);
}
},
@@ -1387,37 +1342,6 @@ export default {
});
},
/**
* Triggers a tooltip to display when a feature was attempted to be used
* while in audio only mode.
*
* @param {string} featureName - The name of the feature that attempted to
* toggle.
* @private
* @returns {void}
*/
_displayAudioOnlyTooltip(featureName) {
let buttonName = null;
let tooltipElementId = null;
switch (featureName) {
case 'screenShare':
buttonName = 'desktop';
tooltipElementId = 'screenshareWhileAudioOnly';
break;
case 'videoMute':
buttonName = 'camera';
tooltipElementId = 'unmuteWhileAudioOnly';
break;
}
if (tooltipElementId) {
APP.UI.showToolbar(6000);
APP.UI.showCustomToolbarPopup(
buttonName, tooltipElementId, true, 5000);
}
},
/**
* Returns whether or not the conference is currently in audio only mode.
*
@@ -1528,8 +1452,6 @@ export default {
}
if (this.isAudioOnly()) {
this._displayAudioOnlyTooltip('screenShare');
return Promise.reject('No screensharing in audio only mode');
}
@@ -1871,9 +1793,6 @@ export default {
room.on(JitsiConferenceEvents.TALK_WHILE_MUTED, () => {
APP.UI.showToolbar(6000);
APP.UI.showCustomToolbarPopup(
'microphone', 'talkWhileMutedPopup', true, 5000);
});
room.on(
@@ -1896,21 +1815,14 @@ export default {
});
room.on(JitsiConferenceEvents.DOMINANT_SPEAKER_CHANGED, id => {
APP.store.dispatch(dominantSpeakerChanged(id));
if (this.isLocalId(id)) {
this.isDominantSpeaker = true;
this.setRaisedHand(false);
} else {
this.isDominantSpeaker = false;
const participant = room.getParticipantById(id);
if (participant) {
APP.UI.setRaisedHandStatus(participant, false);
}
}
APP.UI.markDominantSpeaker(id);
});
room.on(JitsiConferenceEvents.LIVE_STREAM_URL_CHANGED,
(from, liveStreamViewURL) =>
APP.store.dispatch(updateRecordingState({
liveStreamViewURL
})));
if (!interfaceConfig.filmStripOnly) {
room.on(JitsiConferenceEvents.CONNECTION_INTERRUPTED, () => {
APP.UI.markVideoInterrupted(true);
@@ -1963,10 +1875,6 @@ export default {
reportError(e);
}
});
APP.UI.addListener(
UIEvents.VIDEO_UNMUTING_WHILE_AUDIO_ONLY,
() => this._displayAudioOnlyTooltip('videoMute'));
}
room.on(JitsiConferenceEvents.CONNECTION_INTERRUPTED, () => {
@@ -2022,7 +1930,10 @@ export default {
(participant, name, oldValue, newValue) => {
switch (name) {
case 'raisedHand':
APP.UI.setRaisedHandStatus(participant, newValue);
APP.store.dispatch(participantUpdated({
id: participant.getId(),
raisedHand: newValue === 'true'
}));
break;
case 'remoteControlSessionStatus':
APP.UI.setRemoteControlActiveStatus(
@@ -2081,26 +1992,13 @@ export default {
}
});
room.on(
JitsiConferenceEvents.DTMF_SUPPORT_CHANGED,
isDTMFSupported => {
APP.UI.updateDTMFSupport(isDTMFSupported);
}
);
APP.UI.addListener(UIEvents.AUDIO_MUTED, muted => {
this.muteAudio(muted);
});
APP.UI.addListener(UIEvents.VIDEO_MUTED, muted => {
if (this.isAudioOnly() && !muted) {
this._displayAudioOnlyTooltip('videoMute');
} else {
this.muteVideo(muted);
}
this.muteVideo(muted);
});
statsEmitter.startListeningForStats(room);
room.addCommandListener(this.commands.defaults.ETHERPAD,
({ value }) => {
APP.UI.initEtherpad(value);
@@ -2198,13 +2096,6 @@ export default {
room.toggleRecording(options);
});
APP.UI.addListener(UIEvents.SUBJECT_CHANGED, topic => {
room.setSubject(topic);
});
room.on(JitsiConferenceEvents.SUBJECT_CHANGED, subject => {
APP.UI.setSubject(subject);
});
APP.UI.addListener(UIEvents.AUTH_CLICKED, () => {
AuthHandler.authenticate(room);
});
@@ -2361,6 +2252,8 @@ export default {
}
});
}
APP.store.dispatch(setSharedVideoStatus(state));
});
room.addCommandListener(
this.commands.defaults.SHARED_VIDEO,
@@ -2623,30 +2516,6 @@ export default {
APP.API.notifyVideoAvailabilityChanged(available);
},
/**
* Toggles the local "raised hand" status.
*/
maybeToggleRaisedHand() {
this.setRaisedHand(!this.isHandRaised);
},
/**
* Sets the local "raised hand" status to a particular value.
*/
setRaisedHand(raisedHand) {
if (raisedHand !== this.isHandRaised) {
APP.UI.onLocalRaiseHandChanged(raisedHand);
this.isHandRaised = raisedHand;
// Advertise the updated status
room.setLocalParticipantProperty('raisedHand', raisedHand);
// Update the view
APP.UI.setLocalRaisedHandStatus(raisedHand);
}
},
/**
* Disconnect from the conference and optionally request user feedback.
* @param {boolean} [requestFeedback=false] if user feedback should be

View File

@@ -57,6 +57,9 @@ var config = {
// P2P test mode disables automatic switching to P2P when there are 2
// participants in the conference.
p2pTestMode: false
// Enables the test specific features consumed by jitsi-meet-torture
// testMode: false
},
// Disables ICE/UDP by filtering out local and remote UDP candidates in
@@ -178,6 +181,23 @@ var config = {
// Disables or enables RTX (RFC 4588) (defaults to false).
// disableRtx: false,
// Disables or enables TCC (the default is in Jicofo and set to true)
// (draft-holmer-rmcat-transport-wide-cc-extensions-01). This setting
// affects congestion control, it practically enables send-side bandwidth
// estimations.
// enableTcc: true,
// Disables or enables REMB (the default is in Jicofo and set to false)
// (draft-alvestrand-rmcat-remb-03). This setting affects congestion
// control, it practically enables recv-side bandwidth estimations. When
// both TCC and REMB are enabled, TCC takes precedence. When both are
// disabled, then bandwidth estimations are disabled.
// enableRemb: false,
// Defines the minimum number of participants to start a call (the default
// is set in Jicofo and set to 2).
// minParticipants: 2,
// Use XEP-0215 to fetch STUN and TURN servers.
// useStunTurn: true,
@@ -322,7 +342,6 @@ var config = {
// List of undocumented settings used in jitsi-meet
/**
alwaysVisibleToolbar
autoEnableDesktopSharing
autoRecord
autoRecordToken
debug

View File

@@ -2,42 +2,6 @@
* Project animations
**/
/**
* Slide in animation for extended toolbar.
*/
@include keyframes(slideInX) {
0% { transform: translateX(-100%); }
100% { transform: translateX(0%); }
}
@include keyframes(slideOutX) {
0% { transform: translateX(0%); }
100% { transform: translateX(-100%); }
}
@include keyframes(slideInExtX) {
0% { transform: translateX(-500%); }
100% { transform: translateX(0%); }
}
@include keyframes(slideOutExtX) {
0% { transform: translateX(0%); }
100% { transform: translateX(-500%); }
}
/**
* Slide in / out animation for main toolbar.
*/
@include keyframes(slideInY) {
100% { transform: translateY(0%); }
}
@include keyframes(slideOutY) {
0% { transform: translateY(0%); }
100% { transform: translateY(-100%); }
}
/**
* Slide in animation for extended toolbar (inner) panel.
*/
@@ -53,17 +17,3 @@
from { left: 0; }
to { left: -$sidebarWidth; }
}
/**
* Slide in animation for extended toolbar container
**/
@include keyframes(slideOutExtContainer) {
from { width: $sidebarWidth; }
to { width: 0; }
}
@include keyframes(slideInExtContainer) {
from { width: 0; }
to { width: $sidebarWidth; }
}

View File

@@ -3,11 +3,17 @@
* none is applied. Other browsers already support selecting within inputs while
* user-select is none. As such, disallow user-select except on inputs.
*/
*:not(input) {
* {
-webkit-user-select: none;
user-select: none;
}
input,
textarea {
-webkit-user-select: text;
user-select: text;
}
body {
margin: 0px;
width: 100%;
@@ -22,6 +28,16 @@ body {
}
}
/**
* AtlasKitThemeProvider sets a background color on an app-wrapping div, thereby
* preventing transparency in filmstrip-only mode. The selector chosen to
* override this behavior is specific to where the AtlasKitThemeProvider might
* be placed within the app hierarchy.
*/
.filmstrip-only #react > .ckAJgx {
background: transparent;
}
p {
margin: 0;
}

View File

@@ -62,6 +62,36 @@
.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;
}
}
.errorMessage {
color: red;

View File

@@ -1,59 +0,0 @@
#contacts_container {
cursor: default;
#contacts {
font-size: 12px;
bottom: 0px;
margin: 0;
margin-top: 12px;
padding: 0px;
width: 100%;
}
.clickable {
cursor: pointer;
}
}
#contacts {
.contact-list-item {
align-items: center;
border-radius: 3px;
color: $baseLight;
display: flex;
font-size: 14px;
height: 36px;
list-style-type: none;
padding: 0 10%;
text-align: left;
white-space: nowrap;
&:hover,
&:active {
background: $toolbarSelectBackground;
}
.contact-list-item-name {
overflow: hidden;
text-overflow: ellipsis;
}
.avatar {
cursor: pointer;
height: 24px;
margin: 0 8px 0 4px;
width: 24px;
}
}
}
.avatar {
padding: 0px;
margin-right: 10px;
vertical-align: middle;
font-size: 22pt;
border-radius: 20px;
max-height: 30px;
max-width: 30px;
}

View File

@@ -5,6 +5,20 @@
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;
@@ -66,37 +80,6 @@
&#filmstripLocalVideo {
align-self: flex-end;
display: block;
/**
* The invite button style.
*/
.filmstrip__invite {
padding-bottom: 5px;
margin-left: 2px;
}
/**
* The invite button group style.
* TOFIX: use AtlasKit.ButtonGroup if it starts supporting different
* flex grow options for the buttons.
*/
.invite-button-group {
display: inline-flex;
justify-content: space-between;
width: 100%;
& button {
flex-grow: 1;
flex-shrink: 1;
overflow: hidden;
}
& > * {
flex-grow: 0;
flex-shrink: 0;
margin-left: 2px;
}
}
}
&.hidden {

View File

@@ -180,3 +180,18 @@
.icon-gsm-bars:before {
content: "\e926";
}
.icon-open_in_new:before {
content: "\e89e";
}
.icon-AUD:before {
content: "\e900";
}
.icon-HD:before {
content: "\e927";
}
.icon-LD:before {
content: "\e928";
}
.icon-SD:before {
content: "\e929";
}

View File

@@ -1,6 +1,37 @@
/**
* Toolbar side panel main container element.
*/
.use-new-toolbox #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%;
@@ -44,7 +75,7 @@
}
/**
* Inner container, for example contact list, settings or profile.
* Inner container, for example settings or profile.
*/
.sideToolbarContainer__inner {
display: none;

View File

@@ -19,361 +19,275 @@
vertical-align: middle;
}
.toolbar-container {
display: block;
left:0;
min-height: 100px;
opacity: 0;
pointer-events: none;
position: absolute;
right:0;
text-align: center;
top:0;
z-index: $toolbarZ;
.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);
}
}
/**
* Common toolbar styles.
* TODO: when the old filmstrip has been removed, remove the "new-" prefix.
*/
.toolbar {
height: 100%;
pointer-events: auto;
position: relative;
.new-toolbox {
background-color: $newToolbarBackgroundColor;
bottom: calc((#{$newToolbarSize} * 2) * -1);
box-sizing: border-box;
display: flex;
justify-content: space-between;
padding: 16px 8px;
position: absolute;
transition: bottom .3s ease-in;
width: 100%;
z-index: $toolbarZ;
/**
* Ensure nested elements that don't have a button class, maybe because they
* are wrapped in a React Element, still display in a line.
*/
> div {
display: inline-block;
&.visible {
bottom: 0;
}
/**
* Always on top overrides.
*/
&.always-on-top {
/**
* Toolbar button styles for always on top.
*/
.button {
font-size: $alwaysOnTopToolbarFontSize;
height: $alwaysOnTopToolbarSize;
line-height: $alwaysOnTopToolbarSize;
width: $alwaysOnTopToolbarSize;
&_hangup, &_hangup:hover {
font-size: $alwaysOnTopToolbarFontSize;
}
}
&.no-buttons {
display: none;
}
/**
* Toolbar button styles.
*/
.button {
color: $toolbarButtonColor;
cursor: pointer;
z-index: $zindex1;
display: inline-block;
font-size: $toolbarFontSize;
height: $defaultToolbarSize;
line-height: $defaultToolbarSize;
position: relative;
text-align: center;
top:0px;
vertical-align: middle;
width: $defaultToolbarSize;
&[disabled] {
opacity: 0.5;
}
&:hover, &:active {
color: $toolbarButtonColor;
cursor: pointer;
text-decoration: none;
}
&_hangup, &_hangup:hover {
color: $hangupColor;
font-size: $hangupFontSize;
}
&:not(.toggled) {
&:hover, &:active {
// sum opacity with background layer should give us 0.8
background: $toolbarSelectBackground;
}
}
&.toggled {
background: $toolbarToggleBackground;
&.icon-camera {
@extend .icon-camera-disabled;
}
&.icon-full-screen {
@extend .icon-exit-full-screen;
}
&.icon-microphone {
@extend .icon-mic-disabled;
}
}
&.unclickable {
cursor: default;
&:hover, &:active, &.selected {
background: none;
cursor: default;
}
}
}
/**
* Primary toolbar styles.
*/
&_primary {
background-color: $toolbarBackground;
position: absolute;
left: 50%;
top: 30px;
display: inline-block;
width: auto;
height: $defaultToolbarSize;
border-radius: 3px;
opacity: 0;
&.always-on-top {
height: $alwaysOnTopToolbarSize;
top: 10px;
}
@include transform(translateX(-50%));
> a:first-child.button,
> div:first-child .button {
border-bottom-left-radius: 3px;
border-top-left-radius: 3px;
}
> a:last-child.button,
> div:last-child .button {
border-bottom-right-radius: 3px;
border-top-right-radius: 3px;
}
}
&_primary a.button:last-child::after {
content: none;
}
/**
* Secondary toolbar styles.
*/
&_secondary {
background-color: $secondaryToolbarBg;
position: absolute;
align-items: center;
box-sizing: border-box;
display: -moz-box;
display: -ms-flexbox;
display: -webkit-box;
display: -webkit-flex;
.button-group-center,
.button-group-left,
.button-group-right {
display: flex;
flex-direction: column;
flex-wrap: nowrap;
width: 33%;
}
.button-group-center {
justify-content: center;
}
.button-group-right {
justify-content: flex-end;
}
/**
* Overwrite font-awesome styling to match jitsi-icon styling.
*/
.fa {
height: 24px;
margin-left: 2px;
width: 24px;
}
i {
border-radius: 5px;
cursor: pointer;
display: block;
font-size: inherit;
height: 100%;
justify-content: flex-start;
left: 0;
padding-top: 24px;
pointer-events: none;
top: 0;
transform: translateX(-100%);
width: $defaultToolbarSize;
-webkit-transform: translateX(-100%);
line-height: inherit;
width: 100%;
}
.button {
font-size: $secToolbarFontSize;
height: $secToolbarLineHeight;
line-height: $secToolbarLineHeight;
}
i:hover {
background-color: $newToolbarButtonHoverColor;
}
> * {
pointer-events: auto
}
i.toggled {
background: $newToolbarButtonToggleColor;
}
.button.toggled:not(.icon-raised-hand):not(.button-active) {
background: $secondaryToolbarBg;
i.toggled:hover {
background-color: $newToolbarButtonHoverColor;
}
i.disabled {
cursor: initial
}
i.disabled:hover {
background-color: initial;
}
.icon-hangup {
color: $hangupColor;
}
.overflow-menu {
font-size: 1.2em;
list-style-type: none;
/**
* Undo atlaskit padding by reducing margins.
*/
margin: -15px -24px;
padding: 4px 0;
.overflow-menu-item {
align-items: center;
color: #B8C7E0;
cursor: pointer;
text-decoration: none;
display: flex;
font-size: 14px;
height: 22px;
padding: 5px 12px;
&:hover {
background: #313D52;
}
&.unclickable {
cursor: default;
&:hover, &:active, &.selected {
background: none;
cursor: default;
}
}
&.unclickable:hover {
background: inherit;
}
}
.overflow-menu-item-icon {
margin-right: 10px;
i {
display: inline;
font-size: 24px;
}
i:hover {
background-color: initial;
}
img {
max-width: 24px;
max-height: 24px;
}
}
.profile-text {
max-width: 150px;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
}
/**
* Styles the toolbar in filmstrip-only mode.
*/
&_filmstrip-only {
background-color: $toolbarBackground;
border-radius: 3px;
.toolbox-button {
color: $toolbarButtonColor;
cursor: pointer;
display: inline-block;
height: auto;
width: $defaultFilmStripOnlyToolbarSize;
line-height: $newToolbarSize;
margin: 0 4px;
text-align: center;
}
.button {
height: 37px;
line-height: 37px !important;
width: 37px;
}
.toolbar-button-with-badge {
position: relative;
.button-popover-message {
width: 100px;
}
.toolbar-button-wrapper:first-child .button {
border-top-left-radius: 3px;
border-top-right-radius: 3px;
}
.toolbar-button-wrapper:last-child .button {
border-bottom-right-radius: 3px;
border-bottom-left-radius: 3px;
.badge-round {
bottom: -5px;
font-size: 12px;
line-height: 20px;
min-width: 20px;
pointer-events: none;
position: absolute;
right: -5px;
}
}
/**
* Toolbar specific round badge.
*/
.badge-round {
bottom: 9px;
position: absolute;
right: 9px;
.toolbox-button-wth-dialog {
display: inline-block;
}
.toolbox-icon {
height: $newToolbarSize;
font-size: 24px;
width: $newToolbarSize;
}
}
.filmstrip-only {
.toolbox,
.toolbox-toolbars {
align-items: center;
display: flex;
.always-on-top-toolbox,
.filmstrip-toolbox {
background-color: $newToolbarBackgroundColor;
box-sizing: border-box;
display: flex;
flex-direction: column;
z-index: $toolbarZ;
i {
cursor: pointer;
display: block;
}
i:hover {
background-color: $newToolbarButtonHoverColor;
}
i.toggled {
background: $newToolbarButtonToggleColor;
}
i.toggled:hover:not(.disabled) {
background-color: $newToolbarButtonHoverColor;
}
.icon-hangup {
color: $hangupColor;
}
.toolbox-button {
color: $toolbarButtonColor;
cursor: pointer;
text-align: center;
}
border-radius: 3px;
}
.always-on-top-toolbox {
flex-direction: row;
left: 50%;
position: absolute;
top: 10px;
transform: translateX(-50%);
z-index: $toolbarZ;
i {
font-size: $alwaysOnTopToolbarFontSize;
height: $alwaysOnTopToolbarSize;
line-height: $alwaysOnTopToolbarSize;
width: $alwaysOnTopToolbarSize;
}
.disabled {
cursor: initial;
}
.toolbox-button:first-child i {
border-top-left-radius: 3px;
border-bottom-left-radius: 3px;
}
.toolbox-button:last-child i {
border-top-right-radius: 3px;
border-bottom-right-radius: 3px;
}
}
.subject {
background: linear-gradient(to bottom, rgba(255,255,255,.85) , rgba(255,255,255,.35));
border-bottom-left-radius: 12px;
border-bottom-right-radius: 12px;
box-shadow: 0 0 2px #000000, 0 0 10px #000000;
margin-left: 40%;
margin-right: 40%;
padding: 5px;
position: relative;
text-align: center;
width: auto;
z-index: $zindex3;
&.subject_slide-in {
top: 80px;
@include transition(top .3s ease-in);
.filmstrip-toolbox {
i {
font-size: 1.9em;
height: 37px;
line-height: 37px;
width: 37px;
}
&.subject_slide-out {
top: 0;
@include transition(top .3s ease-out);
.toolbox-button:first-child i {
border-top-left-radius: 3px;
border-top-right-radius: 3px;
}
.toolbox-button:last-child i {
border-bottom-left-radius: 3px;
border-bottom-right-radius: 3px;
}
}
#toolbar_button_profile {
height: $toolbarAvatarSize + 2*$toolbarAvatarPadding;
}
a.button>#avatar {
border-radius: 50%;
padding-bottom: $toolbarAvatarPadding;
padding-top: $toolbarAvatarPadding;
width: $toolbarAvatarSize;
}
#feedbackButton {
margin-top: auto;
}
/**
* START of slide in animation for extended toolbar.
*/
@include keyframes(slideInX) {
0% { transform: translateX(-100%); }
100% { transform: translateX(0%); }
}
.slideInX {
@include animation('slideInX .5s forwards');
}
@include keyframes(slideOutX) {
0% { transform: translateX(0%); }
100% { transform: translateX(-100%); }
}
.slideOutX {
@include animation('slideOutX .5s forwards');
}
@include keyframes(slideInExtX) {
0% { transform: translateX(-500%); }
100% { transform: translateX(0%); }
}
.slideInExtX {
@include animation('slideInExtX .5s forwards');
}
@include keyframes(slideOutExtX) {
0% { transform: translateX(0%); }
100% { transform: translateX(-500%); }
}
.slideOutExtX {
@include animation('slideOutExtX .5s forwards');
}
/**
* END of slide out animation for extended toolbar.
*/
/**
* START of slide in / out animation for main toolbar.
*/
@include keyframes(slideInY) {
100% { transform: translateY(0%); }
}
.slideInY {
@include animation('slideInY .5s forwards');
}
@include keyframes(slideOutY) {
0% { transform: translateY(0%); }
100% { transform: translateY(-100%); }
}
.slideOutY {
@include animation('slideOutY .5s forwards');
}
/**
* END of slide in / out animation for main toolbar.
*/
/**
* START of slide in animation for extended toolbar panel.
*/

View File

@@ -35,21 +35,14 @@ $defaultDarkColor: #2b3d5c;
$alwaysOnTopToolbarFontSize: 1em;
$alwaysOnTopToolbarSize: 30px;
$defaultToolbarSize: 50px;
$defaultFilmStripOnlyToolbarSize: 37px;
$secToolbarFontSize: 1.9em;
$secToolbarLineHeight: 45px;
$toolbarAvatarPadding: 10px;
$toolbarAvatarSize: 40px;
$toolbarFontSize: 1.9em;
$newToolbarBackgroundColor: rgba(22, 38, 55, 0.8);
$newToolbarButtonHoverColor: rgba(14, 20, 35, 0.6);
$newToolbarButtonToggleColor: rgba(14, 20, 35, 1);
$newToolbarFontSize: 1.9em;
$newToolbarSize: 32px;
$newToolbarSizeWithPadding: calc(32px + 32px);
$toolbarTitleFontSize: 19px;
/**
* Main controls
* TODO: looks like we don't use it
*/
$inputSemiBackground: rgba(132, 132, 132, .8);
$inputLightBackground: #EBEBEB;
/**
* Video layout
*/
@@ -74,10 +67,6 @@ $feedbackInputBg: #fff;
$feedbackTextColor: #000;
$feedbackInputTextColor: #333;
$feedbackInputPlaceholderColor: #777;
$rateStarLabelColor: #333;
$rateStarDefault: #ccc;
$rateStarActivity: #165ecc;
$rateStarSize: 34px;
/**
* Modals

View File

@@ -19,6 +19,20 @@
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;
@@ -32,14 +46,7 @@
* any parent is also fixed.
*/
position: fixed;
/**
* z-index adjusting is needed because the video state indicator has to
* display over the filmstrip when no videos are displayed but still be
* clickable but its inline dialogs must display over the video state
* indicator when videos are displayed.
*/
z-index: #{$tooltipsZ + 1};
z-index: $filmstripVideosZ;
/**
* Hide videos by making them slight to the right.
@@ -59,8 +66,9 @@
&#remoteVideos {
/**
* Remove horizontal filmstrip padding used to prevent videos
* from overlapping the left-side toolbar. An id selector is
* used to match id specificity with filmstrip styles.
* 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;
@@ -68,8 +76,7 @@
}
/**
* Re-styles the local Video and invite button to better fit the
* vertical filmstrip layout.
* Re-styles the local Video to better fit vertical filmstrip layout.
*/
#filmstripLocalVideo {
align-self: initial;
@@ -78,12 +85,6 @@
flex-direction: column-reverse;
height: auto;
justify-content: flex-start;
.filmstrip__invite {
padding-bottom: 0px;
padding-top: 5px;
z-index: $dropdownZ;
}
}
/**
@@ -168,20 +169,6 @@
.filmstrip__videos-filmstripOnly {
height: 100%;
}
/**
* In filmstrip only mode, the toolbar is normally displayed in the
* vertical center of the filmstrip strip. In vertical filmstrip mode,
* that alignment makes the toolbar appear floating and detached from
* the filmstrip. So, instead anchor the toolbar next to the local
* video.
*/
.toolbar_filmstrip-only {
bottom: 0;
top: auto;
transform: none;
}
}
/**

View File

@@ -1,7 +1,11 @@
body.welcome-page {
background: inherit;
overflow: auto;
}
.welcome {
font-family: $welcomePageFontFamily;
height: 100%;
overflow: auto;
position: relative;
.header {
@@ -19,6 +23,7 @@
justify-content: space-around;
margin-top: 120px;
margin-bottom: 20px;
max-width: calc(100% - 40px);
min-height: 286px;
width: 645px;
}
@@ -50,6 +55,7 @@
align-items: center;
display: flex;
margin-bottom: 20px;
max-width: calc(100% - 40px);
position: relative;
z-index: 2;

View File

@@ -0,0 +1,75 @@
.deep-linking-desktop {
background-color: #fff;
width: 100%;
height: 100%;
display: flex;
flex-flow: column;
.header {
width: 100%;
height: 55px;
background-color: #f1f2f5;
padding-top: 15px;
padding-left: 50px;
display: flex;
flex-flow: row;
flex: 0 0 55px;
.logo {
height: 40px;
}
}
.content {
padding-top: 40px;
padding-bottom: 40px;
left: 0px;
right: 0px;
display: flex;
width: 100%;
height: 100%;
flex-flow: row;
.leftColumn {
left: 0px;
width: 50%;
min-height: 156px;
display: flex;
flex-flow: column;
.leftColumnContent{
padding: 20px;
display: flex;
flex-flow: column;
height: 100%;
.image {
background-image: url('../images/deep-linking-image.png');
background-repeat: no-repeat;
background-position: center;
background-size: contain;
height: 100%;
width: 100%;
}
}
}
.rightColumn {
top: 0px;
width: 50%;
min-height: 156px;
display: flex;
flex-flow: row;
align-items: center;
.rightColumnContent {
display: flex;
flex-flow: column;
padding: 20px 20px 20px 60px;
.title {
color: #1c2946;
}
.description {
color: #606a80;
margin-top: 8px;
}
.buttons {
margin-top: 16px;
}
}
}
}
}

View File

@@ -0,0 +1,3 @@
@import 'desktop';
@import 'mobile';
@import 'no-mobile-app';

View File

@@ -1,10 +1,23 @@
.unsupported-mobile-browser {
.deep-linking-mobile {
background-color: #fff;
height: 100vh;
overflow: auto;
position: relative;
width: 100vw;
.header {
width: 100%;
height: 70px;
background-color: #f1f2f5;
text-align: center;
.logo {
margin-top: 15px;
margin-left: auto;
margin-right: auto;
height: 40px;
}
}
a {
text-decoration: none
}
@@ -20,10 +33,19 @@
a:active {
text-decoration: none;
}
.image {
max-width: 80%;
}
}
&__text {
font-weight: bolder;
padding: 10px 10px 0px 10px;
}
&__text,
.unsupported-dial-in {
.deep-linking-dial-in {
font-size: 1.2em;
line-height: em(29px, 21px);
margin-bottom: 0.65em;
@@ -39,21 +61,27 @@
}
}
&__logo {
height: 108px;
width: 77px;
&__href {
height: 2.2857142857142856em;
line-height: 2.2857142857142856em;
margin: 18px auto 20px;
max-width: 300px;
width: auto;
font-weight: bolder;
}
&__button {
border: 0;
height: 2.2857142857142856em;
line-height: 2.2857142857142856em;
margin: 18px auto 20px;
margin: 18px auto 10px;
padding: 0px 10px 0px 10px;
max-width: 300px;
width: auto;
@include border-radius(3px);
background-color: $unsupportedBrowserButtonBgColor;
color: #505F79;
font-weight: bold;
&:active {
background-color: $unsupportedBrowserButtonBgColor;
@@ -69,7 +97,7 @@
}
}
.unsupported-dial-in {
.deep-linking-dial-in {
display: none;
&.has-numbers {

View File

@@ -48,7 +48,6 @@
@import 'popup_menu';
@import 'recording';
@import 'login_menu';
@import 'contact_list';
@import 'chat';
@import 'ringing/ringing';
@import 'welcome_page';
@@ -72,5 +71,6 @@
@import 'unsupported-browser/main';
@import 'modals/invite/add-people';
@import 'vertical_filmstrip_overrides';
@import 'deep-linking/main';
/* Modules END */

View File

@@ -47,10 +47,6 @@
.feedback-dialog {
.details {
margin-top: 20px;
padding-left: 60px;
padding-right: 60px;
textarea {
min-height: 100px;
}
@@ -77,16 +73,15 @@
text-align: center;
.star-label {
color: $rateStarLabelColor;
font-size: 14px;
height: 16px;
}
.star-btn {
color: $rateStarDefault;
color: inherit;
cursor: pointer;
display: inline-block;
font-size: $rateStarSize;
font-size: 34px;
outline: none;
position: relative;
text-decoration: none;
@@ -95,7 +90,7 @@
&.active,
&:hover,
&.starHover {
color: $rateStarActivity;
color: #36B37E;
};
}

View File

@@ -13,7 +13,15 @@
}
.add-telephone-icon {
display: flex;
height: 28px;
transform: scaleX(-1);
width: 28px;
i {
line-height: 28px;
margin: auto;
}
}
}
}

View File

@@ -1,3 +1,7 @@
.use-new-toolbox {
font-size: 14px;
}
.info-dialog {
cursor: default;
display: flex;
@@ -38,7 +42,7 @@
.info-dialog-copy-element {
opacity: 0;
pointer-events: none;
position: fixed;
position: absolute;
-webkit-user-select: text;
user-select: text;
}
@@ -55,12 +59,13 @@
}
}
.info-dialog-conference-url {
max-width: 250px;
overflow: hidden;
text-overflow: ellipsis;
user-select: text;
white-space: nowrap;
.info-dialog-conference-url,
.info-dialog-live-stream-url {
width: max-content;
width: -moz-max-content;
width: -webkit-max-content;
word-break: break-all;
max-width: 400px;
}
.info-dialog-dial-in {
@@ -77,19 +82,29 @@
font-size: 16px;
}
.info-dialog-url-text,
.info-dialog-url-text:hover {
color: inherit;
cursor: inherit;
}
.info-dialog-title {
font-weight: bold;
margin-bottom: 10px;
}
.info-password,
.info-dialog-password,
.info-password,
.info-password-form {
align-items: baseline;
display: flex;
}
.info-label {
font-weight: bold;
}
.info-password-field {
margin-left: 2px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
@@ -110,10 +125,6 @@
.info-password-local {
user-select: text;
}
.conference-id {
margin-left: 5px;
}
}
.dial-in-page {
@@ -125,10 +136,6 @@
justify-content: center;
width: 100%;
* {
user-select: text;
}
.dial-in-numbers-list {
font-size: 24px;
margin-top: 20px;
@@ -139,3 +146,12 @@
width: 30%;
}
}
.info-dialog,
.dial-in-page {
* {
user-select: text;
-moz-user-select: text;
-webkit-user-select: text;
}
}

View File

@@ -135,6 +135,12 @@
}
}
.modal-dialog-form {
.video-quality-dialog-title {
display: none;
}
}
.video-state-indicator {
background: $videoStateIndicatorBackground;
cursor: default;
@@ -162,11 +168,11 @@
}
.centeredVideoLabel.moveToCorner {
z-index: $tooltipsZ;
z-index: $zindex3;
}
#videoResolutionLabel {
z-index: #{$tooltipsZ + 1};
z-index: $zindex3 + 1;
}
.centeredVideoLabel {

View File

@@ -1,3 +1 @@
@import 'no-mobile-app';
@import 'unsupported-desktop-browser';
@import 'unsupported-mobile-browser';

View File

@@ -97,11 +97,6 @@ api.executeCommand('toggleFilmStrip')
api.executeCommand('toggleChat')
```
* **toggleContactList** - Hides / shows the contact list. No arguments are required.
```javascript
api.executeCommand('toggleContactList')
```
* **toggleShareScreen** - Starts / stops screen sharing. No arguments are required.
```javascript
api.executeCommand('toggleShareScreen')

View File

@@ -20,6 +20,14 @@ wget -qO - https://download.jitsi.org/jitsi-key.gpg.key | apt-key add -
apt-get update
```
(If you get an error:
E: The method driver /usr/lib/apt/methods/https could not be found.
run:
```sh
apt-get install apt-transport-https
```
)
### Install Jitsi Meet
Note : Something to consider before installation is how you're planning to serve Jitsi Meet. The installer will check if Nginx or Apache is present (with this order) and configure a virtualhost within the web server it finds to serve Jitsi Meet. If none of the above is found it then configures itself to be served via jetty. So if for example you are planning on deploying Jitsi Meet with a web server, you have to make sure to install the server **before** installing jitsi-meet.

Binary file not shown.

View File

@@ -23,7 +23,9 @@
<glyph unicode="&#xe616;" glyph-name="event_note" d="M598 426v-84h-300v84h300zM810 214v468h-596v-468h596zM810 896c46 0 86-40 86-86v-596c0-46-40-86-86-86h-596c-48 0-86 40-86 86v596c0 46 38 86 86 86h42v86h86v-86h340v86h86v-86h42zM726 598v-86h-428v86h428z" />
<glyph unicode="&#xe61d;" glyph-name="phone-talk" d="M640 512c0 70-58 128-128 128v86c118 0 214-96 214-214h-86zM810 512c0 166-132 298-298 298v86c212 0 384-172 384-384h-86zM854 362c24 0 42-18 42-42v-150c0-24-18-42-42-42-400 0-726 326-726 726 0 24 18 42 42 42h150c24 0 42-18 42-42 0-54 8-104 24-152 4-14 2-32-10-44l-94-94c62-122 162-220 282-282l94 94c12 12 30 14 44 10 48-16 98-24 152-24z" />
<glyph unicode="&#xe80b;" glyph-name="public" d="M764 282c56 60 90 142 90 230 0 142-88 266-214 316v-18c0-46-40-84-86-84h-84v-86c0-24-20-42-44-42h-84v-86h256c24 0 42-18 42-42v-128h42c38 0 70-26 82-60zM470 174v82c-46 0-86 40-86 86v42l-204 204c-6-24-10-50-10-76 0-174 132-318 300-338zM512 938c236 0 426-190 426-426s-190-426-426-426-426 190-426 426 190 426 426 426z" />
<glyph unicode="&#xe89e;" glyph-name="open_in_new" d="M598 896h298v-298h-86v152l-418-418-60 60 418 418h-152v86zM810 214v298h86v-298c0-46-40-86-86-86h-596c-48 0-86 40-86 86v596c0 46 38 86 86 86h298v-86h-298v-596h596z" />
<glyph unicode="&#xe8b3;" glyph-name="restore" d="M512 682h64v-180l150-90-32-52-182 110v212zM554 896c212 0 384-172 384-384s-172-384-384-384c-106 0-200 42-270 112l60 62c54-54 128-88 210-88 166 0 300 132 300 298s-134 298-300 298-298-132-298-298h128l-172-172-4 6-166 166h128c0 212 172 384 384 384z" />
<glyph unicode="&#xe900;" glyph-name="AUD" 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-512zM308.25 387.3h57.225l-87.675 252.525h-62.125l-87.675-252.525h53.025l19.425 60.2h88.725l19.075-60.2zM461.9 639.825h-52.85v-165.375c0-56 41.125-93.625 105.7-93.625 64.75 0 105.875 37.625 105.875 93.625v165.375h-52.85v-159.95c0-31.85-19.075-52.15-53.025-52.15-33.775 0-52.85 20.3-52.85 52.15v159.95zM682.225 640v-252.7h99.4c75.6 0 118.475 46.025 118.475 128.1 0 79.1-43.4 124.6-118.475 124.6h-99.4zM735.075 594.85v-162.4h38.15c46.725 0 72.975 28.7 72.975 82.075 0 51.1-27.125 80.325-72.975 80.325h-38.15zM243.5 587.325l-31.675-99.050h66.15l-31.325 99.050h-3.15z" />
<glyph unicode="&#xe901;" glyph-name="avatar" d="M512 204c106 0 200 56 256 138-2 84-172 132-256 132-86 0-254-48-256-132 56-82 150-138 256-138zM512 810c-70 0-128-58-128-128s58-128 128-128 128 58 128 128-58 128-128 128zM512 938c236 0 426-190 426-426s-190-426-426-426-426 190-426 426 190 426 426 426z" />
<glyph unicode="&#xe902;" glyph-name="download" d="M726 470h-128v170h-172v-170h-128l214-214zM826 596c110-8 198-100 198-212 0-118-96-214-214-214h-554c-142 0-256 114-256 256 0 132 100 240 228 254 54 102 160 174 284 174 156 0 284-110 314-258z" />
<glyph unicode="&#xe903;" glyph-name="mic-camera-combined" d="M756.704 628.138l267.296 202.213v-635.075l-267.296 202.213v-191.923c0-12.085-11.296-21.863-25.216-21.863h-706.272c-13.92 0-25.216 9.777-25.216 21.863v612.25c0 12.085 11.296 21.863 25.216 21.863h706.272c13.92 0 25.216-9.777 25.216-21.863v-189.679zM371.338 376.228c47.817 0 86.529 40.232 86.529 89.811v184.835c0 49.651-38.713 89.883-86.529 89.883-47.788 0-86.515-40.232-86.515-89.883v-184.835c0-49.579 38.756-89.811 86.515-89.811v0zM356.754 314.070v-32.78h33.718v33.412c73.858 9.606 131.235 73.73 131.235 151.351v88.232h-30.636v-88.232c0-67.57-53.696-122.534-119.734-122.534-66.024 0-119.691 54.964-119.691 122.534v88.232h-30.636v-88.232c0-79.215 59.674-144.502 135.744-151.969v-0.014z" />
@@ -62,4 +64,7 @@
<glyph unicode="&#xe924;" glyph-name="visibility-off" d="M506 640h6c70 0 128-58 128-128v-8zM322 606c-14-28-24-60-24-94 0-118 96-214 214-214 34 0 66 10 94 24l-66 66c-8-2-18-4-28-4-70 0-128 58-128 128 0 10 2 20 4 28zM86 842l54 54 756-756-54-54c-47.968 47.365-96.266 94.401-144 142-58-24-120-36-186-36-214 0-396 132-470 320 34 84 90 156 160 212-39.017 38.983-77.307 78.693-116 118zM512 726c-28 0-54-6-78-16l-92 92c52 20 110 30 170 30 214 0 394-132 468-320-32-80-82-148-146-202l-124 124c10 24 16 50 16 78 0 118-96 214-214 214z" />
<glyph unicode="&#xe925;" glyph-name="dialpad" d="M512 982c46 0 86-40 86-86s-40-86-86-86-86 40-86 86 40 86 86 86zM512 726c46 0 86-40 86-86s-40-86-86-86-86 40-86 86 40 86 86 86zM768 726c46 0 86-40 86-86s-40-86-86-86-86 40-86 86 40 86 86 86zM768 470c46 0 86-40 86-86s-40-86-86-86-86 40-86 86 40 86 86 86zM512 470c46 0 86-40 86-86s-40-86-86-86-86 40-86 86 40 86 86 86zM768 810c-46 0-86 40-86 86s40 86 86 86 86-40 86-86-40-86-86-86zM256 470c46 0 86-40 86-86s-40-86-86-86-86 40-86 86 40 86 86 86zM256 726c46 0 86-40 86-86s-40-86-86-86-86 40-86 86 40 86 86 86zM256 982c46 0 86-40 86-86s-40-86-86-86-86 40-86 86 40 86 86 86zM512 214c46 0 86-40 86-86s-40-86-86-86-86 40-86 86 40 86 86 86z" />
<glyph unicode="&#xe926;" glyph-name="gsm-bars-black" d="M896 1024c70.692 0 128-57.308 128-128v-768c0-70.692-57.308-128-128-128s-128 57.308-128 128v768c0 70.692 57.308 128 128 128zM512 768c70.692 0 128-57.308 128-128v-512c0-70.692-57.308-128-128-128s-128 57.308-128 128v512c0 70.692 57.308 128 128 128zM128 384v0c70.692 0 128-57.308 128-128v-128c0-70.692-57.308-128-128-128s-128 57.308-128 128v128c0 70.692 57.308 128 128 128v0z" />
<glyph unicode="&#xe927;" glyph-name="HD" 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-512zM481.359 384v255.823h-54.273v-103.18h-116.813v103.18h-54.273v-255.823h54.273v106.903h116.813v-106.903h54.273zM544.258 640v-256h102.077c77.636 0 121.665 46.626 121.665 129.773 0 80.133-44.569 126.227-121.665 126.227h-102.077zM598.531 594.26v-164.521h39.177c47.983 0 74.94 29.075 74.94 83.147 0 51.767-27.855 81.374-74.94 81.374h-39.177z" />
<glyph unicode="&#xe928;" glyph-name="LD" 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-512zM472.4 433.325h-112.35v206.5h-52.85v-252.525h165.2v46.025zM520.35 640v-252.7h99.4c75.6 0 118.475 46.025 118.475 128.1 0 79.1-43.4 124.6-118.475 124.6h-99.4zM573.2 594.85v-162.4h38.15c46.725 0 72.975 28.7 72.975 82.075 0 51.1-27.125 80.325-72.975 80.325h-38.15z" />
<glyph unicode="&#xe929;" glyph-name="SD" 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-512zM281.6 451.175c1.925-47.075 40.95-76.65 101.15-76.65 63.35 0 102.375 31.15 102.375 82.075 0 39.2-21.875 61.075-72.625 71.75l-30.45 6.475c-29.575 6.3-41.65 15.225-41.65 30.8 0 19.25 17.5 31.5 43.925 31.5 25.55 0 44.1-13.3 46.55-33.25h49.7c-1.575 44.975-40.95 76.125-96.6 76.125-58.275 0-96.6-31.325-96.6-78.925 0-38.5 22.575-62.475 68.6-72.1l32.9-7c30.975-6.65 43.575-15.925 43.575-32.025 0-19.075-19.425-32.375-46.9-32.375-29.75 0-50.4 13.125-52.85 33.6h-51.1zM535 633.7v-252.7h99.4c75.6 0 118.475 46.025 118.475 128.1 0 79.1-43.4 124.6-118.475 124.6h-99.4zM587.85 588.55v-162.4h38.15c46.725 0 72.975 28.7 72.975 82.075 0 51.1-27.125 80.325-72.975 80.325h-38.15z" />
</font></defs></svg>

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -23,9 +23,11 @@ var interfaceConfig = {
SHOW_BRAND_WATERMARK: false,
BRAND_WATERMARK_LINK: '',
SHOW_POWERED_BY: false,
SHOW_DEEP_LINKING_IMAGE: false,
GENERATE_ROOMNAMES_ON_WELCOME_PAGE: true,
DISPLAY_WELCOME_PAGE_CONTENT: true,
APP_NAME: 'Jitsi Meet',
NATIVE_APP_NAME: 'Jitsi Meet',
LANG_DETECTION: false, // Allow i18n to detect the system language
INVITATION_POWERED_BY: true,
@@ -44,22 +46,18 @@ var interfaceConfig = {
'microphone', 'camera', 'desktop', 'fullscreen', 'fodeviceselection', 'hangup',
// extended toolbar
'profile', 'contacts', 'info', 'chat', 'recording', 'etherpad', 'sharedvideo', 'settings', 'raisehand', 'videoquality', 'filmstrip' ],
'profile', 'info', 'chat', 'recording', 'etherpad',
'sharedvideo', 'settings', 'raisehand', 'videoquality', 'filmstrip',
'invite', 'feedback', 'stats', 'shortcuts'
],
/**
* Main Toolbar Buttons
* All of them should be in TOOLBAR_BUTTONS
*/
MAIN_TOOLBAR_BUTTONS: [ 'microphone', 'camera', 'desktop', 'fullscreen', 'fodeviceselection', 'hangup' ],
SETTINGS_SECTIONS: [ 'language', 'devices', 'moderator' ],
INVITE_OPTIONS: [ 'invite', 'dialout', 'addtocall' ],
// 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
// screen, 'width' would fit the original video width to the width of the
// screen respecting ratio.
VIDEO_LAYOUT_FIT: 'both',
SHOW_CONTACTLIST_AVATARS: true,
/**
* Whether to only show the filmstrip (and hide the toolbar).
@@ -165,7 +163,7 @@ var interfaceConfig = {
/**
* Specify mobile app scheme for opening the app from the mobile browser.
*/
// MOBILE_APP_SCHEME: 'org.jitsi.meet'
// APP_SCHEME: 'org.jitsi.meet'
};
/* eslint-enable no-unused-vars, no-var, max-len */

View File

@@ -3,7 +3,7 @@ PODS:
- React/Core (= 0.51.0)
- react-native-background-timer (2.0.0):
- React
- react-native-calendar-events (1.4.3):
- react-native-calendar-events (1.5.0):
- React
- react-native-fetch-blob (0.10.6):
- React/Core
@@ -11,7 +11,8 @@ PODS:
- React
- react-native-locale-detector (1.0.0):
- React
- react-native-webrtc (1.58.2)
- react-native-webrtc (1.58.2):
- React
- React/BatchedBridge (0.51.0):
- React/Core
- React/cxxreact_legacy
@@ -43,10 +44,10 @@ PODS:
- React/Core
- React/fishhook
- React/RCTBlob
- RNSound (0.10.4):
- RNSound (0.10.9):
- React/Core
- RNSound/Core (= 0.10.4)
- RNSound/Core (0.10.4):
- RNSound/Core (= 0.10.9)
- RNSound/Core (0.10.9):
- React/Core
- RNVectorIcons (4.4.2):
- React
@@ -102,8 +103,8 @@ SPEC CHECKSUMS:
react-native-fetch-blob: 63394b1d7b0781547b3e4463b3195790177b1222
react-native-keep-awake: 0de4bd66de0c23178107dce0c2fcc3354b2a8e94
react-native-locale-detector: d1b2c6fe5abb56e3a1efb6c2d6f308c05c4251f1
react-native-webrtc: bc044ca9530fc802e7533f247aa08fe1b6bf8dc5
RNSound: d0818fe2435254fe30540fae48a429c5ffb72e09
react-native-webrtc: 31b6d3f1e3e2ce373aa43fd682b04367250f807d
RNSound: b360b3862d3118ed1c74bb9825696b5957686ac4
RNVectorIcons: c0dbfbf6068fefa240c37b0f71bd03b45dddac44
yoga: 17521bbb0dd54a47c0b3ac43253e78cdac7488e0

View File

@@ -255,6 +255,7 @@
13B07F941A680F5B00A75B9A /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIconDebug;
CODE_SIGN_ENTITLEMENTS = app.entitlements;
CURRENT_PROJECT_VERSION = 1;
@@ -286,6 +287,7 @@
13B07F951A680F5B00A75B9A /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIconRelease;
CODE_SIGN_ENTITLEMENTS = app.entitlements;
CURRENT_PROJECT_VERSION = 1;

View File

@@ -55,8 +55,8 @@
</dict>
</dict>
</dict>
<key>NSCalendarsUsageDescription</key>
<string>See your scheduled conferences in the app.</string>
<key>NSCalendarsUsageDescription</key>
<string>See your scheduled conferences in the app.</string>
<key>NSCameraUsageDescription</key>
<string>Participate in conferences with video.</string>
<key>NSLocationWhenInUseUsageDescription</key>

View File

@@ -51,6 +51,8 @@
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>

View File

@@ -21,19 +21,73 @@ class ViewController: UIViewController {
@IBOutlet weak var videoButton: UIButton?
private var jitsiMeetCoordinator: JitsiMeetPresentationCoordinator?
fileprivate var pipViewCoordinator: PiPViewCoordinator?
fileprivate var jitsiMeetView: JitsiMeetView?
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewWillTransition(to size: CGSize,
with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
let rect = CGRect(origin: CGPoint.zero, size: size)
pipViewCoordinator?.resetBounds(bounds: rect)
}
// MARK: - Actions
@IBAction func openJitsiMeet(sender: Any?) {
let jitsiMeetCoordinator = JitsiMeetPresentationCoordinator()
self.jitsiMeetCoordinator = jitsiMeetCoordinator
jitsiMeetCoordinator.jitsiMeetView().welcomePageEnabled = true
jitsiMeetCoordinator.jitsiMeetView().load(nil)
jitsiMeetCoordinator.show()
cleanUp()
// create and configure jitsimeet view
let jitsiMeetView = JitsiMeetView()
jitsiMeetView.welcomePageEnabled = true
jitsiMeetView.pictureInPictureEnabled = true
jitsiMeetView.load(nil)
jitsiMeetView.delegate = self
self.jitsiMeetView = jitsiMeetView
// Enable jitsimeet view to be a view that can be displayed
// on top of all the things, and let the coordinator to manage
// the view state and interactions
pipViewCoordinator = PiPViewCoordinator(withView: jitsiMeetView)
pipViewCoordinator?.configureAsStickyView(withParentView: view)
// animate in
jitsiMeetView.alpha = 0
pipViewCoordinator?.show()
}
fileprivate func cleanUp() {
jitsiMeetView?.removeFromSuperview()
jitsiMeetView = nil
pipViewCoordinator = nil
}
}
extension ViewController: JitsiMeetViewDelegate {
func conferenceFailed(_ data: [AnyHashable : Any]!) {
hideJitsiMeetViewAndCleanUp()
}
func conferenceLeft(_ data: [AnyHashable : Any]!) {
hideJitsiMeetViewAndCleanUp()
}
func enterPicture(inPicture data: [AnyHashable : Any]!) {
DispatchQueue.main.async {
self.pipViewCoordinator?.enterPictureInPicture()
}
}
private func hideJitsiMeetViewAndCleanUp() {
DispatchQueue.main.async {
self.pipViewCoordinator?.hide() { _ in
self.cleanUp()
}
}
}
}

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@@ -27,12 +27,14 @@
0BCA496C1EC4BBF900B793EE /* jitsi.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 0BCA496B1EC4BBF900B793EE /* jitsi.ttf */; };
0BD906EA1EC0C00300C8C18E /* JitsiMeet.h in Headers */ = {isa = PBXBuildFile; fileRef = 0BD906E81EC0C00300C8C18E /* JitsiMeet.h */; settings = {ATTRIBUTES = (Public, ); }; };
0F65EECE1D95DA94561BB47E /* libPods-JitsiMeet.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 03F2ADC957FF109849B7FCA1 /* libPods-JitsiMeet.a */; };
75635B0A20751D6D00F29C9F /* joined.wav in Resources */ = {isa = PBXBuildFile; fileRef = 75635B0820751D6D00F29C9F /* joined.wav */; };
75635B0B20751D6D00F29C9F /* left.wav in Resources */ = {isa = PBXBuildFile; fileRef = 75635B0920751D6D00F29C9F /* left.wav */; };
412BF89D206AA66F0053B9E5 /* InviteSearch.m in Sources */ = {isa = PBXBuildFile; fileRef = 412BF89C206AA66F0053B9E5 /* InviteSearch.m */; };
412BF89F206ABAE40053B9E5 /* InviteSearch.h in Headers */ = {isa = PBXBuildFile; fileRef = 412BF89E206AA82F0053B9E5 /* InviteSearch.h */; settings = {ATTRIBUTES = (Public, ); }; };
C6245F5D2053091D0040BE68 /* image-resize@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = C6245F5B2053091D0040BE68 /* image-resize@2x.png */; };
C6245F5E2053091D0040BE68 /* image-resize@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = C6245F5C2053091D0040BE68 /* image-resize@3x.png */; };
C6A3425F204EF76800E062DD /* PiPWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6A3425C204EF76800E062DD /* PiPWindow.swift */; };
C6A34260204EF76800E062DD /* JitsiMeetPresentationCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6A3425D204EF76800E062DD /* JitsiMeetPresentationCoordinator.swift */; };
C6A34261204EF76800E062DD /* DragGestureController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6A3425E204EF76800E062DD /* DragGestureController.swift */; };
C6A3426D204F1C3300E062DD /* JitsiMeetViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6A3426C204F1C3300E062DD /* JitsiMeetViewController.swift */; };
C6CC49AF207412CF000DFA42 /* PiPViewCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6CC49AE207412CF000DFA42 /* PiPViewCoordinator.swift */; };
C6F99C15204DB63E0001F710 /* JitsiMeetView+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = C6F99C13204DB63D0001F710 /* JitsiMeetView+Private.h */; };
/* End PBXBuildFile section */
@@ -60,14 +62,16 @@
0BD906E51EC0C00300C8C18E /* JitsiMeet.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = JitsiMeet.framework; sourceTree = BUILT_PRODUCTS_DIR; };
0BD906E81EC0C00300C8C18E /* JitsiMeet.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = JitsiMeet.h; sourceTree = "<group>"; };
0BD906E91EC0C00300C8C18E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
75635B0820751D6D00F29C9F /* joined.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; name = joined.wav; path = ../../sounds/joined.wav; sourceTree = "<group>"; };
75635B0920751D6D00F29C9F /* left.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; name = left.wav; path = ../../sounds/left.wav; sourceTree = "<group>"; };
412BF89C206AA66F0053B9E5 /* InviteSearch.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = InviteSearch.m; sourceTree = "<group>"; };
412BF89E206AA82F0053B9E5 /* InviteSearch.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = InviteSearch.h; sourceTree = "<group>"; };
98E09B5C73D9036B4ED252FC /* Pods-JitsiMeet.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JitsiMeet.debug.xcconfig"; path = "../Pods/Target Support Files/Pods-JitsiMeet/Pods-JitsiMeet.debug.xcconfig"; sourceTree = "<group>"; };
9C77CA3CC919B081F1A52982 /* Pods-JitsiMeet.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JitsiMeet.release.xcconfig"; path = "../Pods/Target Support Files/Pods-JitsiMeet/Pods-JitsiMeet.release.xcconfig"; sourceTree = "<group>"; };
C6245F5B2053091D0040BE68 /* image-resize@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "image-resize@2x.png"; path = "src/picture-in-picture/image-resize@2x.png"; sourceTree = "<group>"; };
C6245F5C2053091D0040BE68 /* image-resize@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "image-resize@3x.png"; path = "src/picture-in-picture/image-resize@3x.png"; sourceTree = "<group>"; };
C6A3425C204EF76800E062DD /* PiPWindow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PiPWindow.swift; sourceTree = "<group>"; };
C6A3425D204EF76800E062DD /* JitsiMeetPresentationCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JitsiMeetPresentationCoordinator.swift; sourceTree = "<group>"; };
C6A3425E204EF76800E062DD /* DragGestureController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DragGestureController.swift; sourceTree = "<group>"; };
C6A3426C204F1C3300E062DD /* JitsiMeetViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JitsiMeetViewController.swift; sourceTree = "<group>"; };
C6CC49AE207412CF000DFA42 /* PiPViewCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PiPViewCoordinator.swift; sourceTree = "<group>"; };
C6F99C13204DB63D0001F710 /* JitsiMeetView+Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "JitsiMeetView+Private.h"; sourceTree = "<group>"; };
/* End PBXFileReference section */
@@ -89,6 +93,8 @@
0BCA49681EC4BBE500B793EE /* Resources */ = {
isa = PBXGroup;
children = (
75635B0820751D6D00F29C9F /* joined.wav */,
75635B0920751D6D00F29C9F /* left.wav */,
0BC4B8681F8C01E100CE8B21 /* CallKitIcon.png */,
C6245F5B2053091D0040BE68 /* image-resize@2x.png */,
C6245F5C2053091D0040BE68 /* image-resize@3x.png */,
@@ -123,6 +129,8 @@
0BCA495C1EC4B6C600B793EE /* AudioMode.m */,
0BB9AD7C1F60356D001C08DB /* AppInfo.m */,
0BB9AD7A1F5EC8F4001C08DB /* CallKit.m */,
412BF89E206AA82F0053B9E5 /* InviteSearch.h */,
412BF89C206AA66F0053B9E5 /* InviteSearch.m */,
0BA13D301EE83FF8007BEF7F /* ExternalAPI.m */,
0BD906E91EC0C00300C8C18E /* Info.plist */,
0B7C2CFC200F51D60060D076 /* LaunchOptions.m */,
@@ -165,9 +173,7 @@
isa = PBXGroup;
children = (
C6A3425E204EF76800E062DD /* DragGestureController.swift */,
C6A3425D204EF76800E062DD /* JitsiMeetPresentationCoordinator.swift */,
C6A3426C204F1C3300E062DD /* JitsiMeetViewController.swift */,
C6A3425C204EF76800E062DD /* PiPWindow.swift */,
C6CC49AE207412CF000DFA42 /* PiPViewCoordinator.swift */,
);
path = "picture-in-picture";
sourceTree = "<group>";
@@ -180,6 +186,7 @@
buildActionMask = 2147483647;
files = (
C6F99C15204DB63E0001F710 /* JitsiMeetView+Private.h in Headers */,
412BF89F206ABAE40053B9E5 /* InviteSearch.h in Headers */,
0B412F181EDEC65D00B1A0A6 /* JitsiMeetView.h in Headers */,
0B93EF7E1EC9DDCD0030D24D /* RCTBridgeWrapper.h in Headers */,
0B412F221EDEF6EA00B1A0A6 /* JitsiMeetViewDelegate.h in Headers */,
@@ -252,6 +259,8 @@
0BCA496C1EC4BBF900B793EE /* jitsi.ttf in Resources */,
C6245F5D2053091D0040BE68 /* image-resize@2x.png in Resources */,
0BC4B8691F8C03A700CE8B21 /* CallKitIcon.png in Resources */,
75635B0B20751D6D00F29C9F /* left.wav in Resources */,
75635B0A20751D6D00F29C9F /* joined.wav in Resources */,
C6245F5E2053091D0040BE68 /* image-resize@3x.png in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -337,16 +346,15 @@
buildActionMask = 2147483647;
files = (
0BB9AD7B1F5EC8F4001C08DB /* CallKit.m in Sources */,
C6A34260204EF76800E062DD /* JitsiMeetPresentationCoordinator.swift in Sources */,
0BB9AD7D1F60356D001C08DB /* AppInfo.m in Sources */,
0B93EF7F1EC9DDCD0030D24D /* RCTBridgeWrapper.m in Sources */,
0BA13D311EE83FF8007BEF7F /* ExternalAPI.m in Sources */,
0BCA49601EC4B6C600B793EE /* POSIX.m in Sources */,
0B7C2CFD200F51D60060D076 /* LaunchOptions.m in Sources */,
C6A3426D204F1C3300E062DD /* JitsiMeetViewController.swift in Sources */,
C6A3425F204EF76800E062DD /* PiPWindow.swift in Sources */,
C6CC49AF207412CF000DFA42 /* PiPViewCoordinator.swift in Sources */,
0BCA495F1EC4B6C600B793EE /* AudioMode.m in Sources */,
0B44A0191F902126009D1D64 /* MPVolumeViewManager.m in Sources */,
412BF89D206AA66F0053B9E5 /* InviteSearch.m in Sources */,
0BCA49611EC4B6C600B793EE /* Proximity.m in Sources */,
C6A34261204EF76800E062DD /* DragGestureController.swift in Sources */,
0B412F191EDEC65D00B1A0A6 /* JitsiMeetView.m in Sources */,
@@ -405,7 +413,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 10.3;
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
@@ -458,7 +466,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 10.3;
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";

View File

@@ -29,8 +29,13 @@ RCT_EXPORT_MODULE();
- (NSDictionary *)constantsToExport {
NSDictionary<NSString *, id> *infoDictionary
= [[NSBundle mainBundle] infoDictionary];
// calendarEnabled
BOOL calendarEnabled
= infoDictionary[@"NSCalendarsUsageDescription"] != nil;
// name
NSString *name = infoDictionary[@"CFBundleDisplayName"];
NSString *version = infoDictionary[@"CFBundleShortVersionString"];
if (name == nil) {
name = infoDictionary[@"CFBundleName"];
@@ -38,6 +43,13 @@ RCT_EXPORT_MODULE();
name = @"";
}
}
// sdkBundlePath
NSString *sdkBundlePath = [[NSBundle bundleForClass:self.class] bundlePath];
// version
NSString *version = infoDictionary[@"CFBundleShortVersionString"];
if (version == nil) {
version = infoDictionary[@"CFBundleVersion"];
if (version == nil) {
@@ -46,7 +58,9 @@ RCT_EXPORT_MODULE();
}
return @{
@"calendarEnabled": [NSNumber numberWithBool:calendarEnabled],
@"name": name,
@"sdkBundlePath": sdkBundlePath,
@"version": version
};
};

View File

@@ -0,0 +1,49 @@
/*
* 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.
*/
@class InviteSearchController;
@protocol InviteSearchControllerDelegate
/**
* Called when an InviteSearchController has results for a query that was previously provided.
*/
- (void)inviteSearchController:(InviteSearchController * _Nonnull)controller
didReceiveResults:(NSArray<NSDictionary*> * _Nonnull)results
forQuery:(NSString * _Nonnull)query;
/**
* Called when all invitations were sent successfully.
*/
- (void)inviteDidSucceedForSearchController:(InviteSearchController * _Nonnull)searchController;
/**
* Called when one or more invitations fails to send successfully.
*/
- (void)inviteDidFailForItems:(NSArray<NSDictionary *> * _Nonnull)items
fromSearchController:(InviteSearchController * _Nonnull)searchController;
@end
@interface InviteSearchController: NSObject
@property (nonatomic, nullable, weak) id<InviteSearchControllerDelegate> delegate;
- (void)performQuery:(NSString * _Nonnull)query;
- (void)cancelSearch;
- (void)submitSelectedItemIds:(NSArray<NSString *> * _Nonnull)ids;
@end

215
ios/sdk/src/InviteSearch.m Normal file
View File

@@ -0,0 +1,215 @@
/*
* 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.
*/
#import <React/RCTBridge.h>
#import <React/RCTEventEmitter.h>
#import <React/RCTUtils.h>
#import "JitsiMeetView+Private.h"
#import "InviteSearch.h"
// The events emitted/supported by InviteSearch:
static NSString * const InviteSearchPerformQueryAction = @"performQueryAction";
static NSString * const InviteSearchPerformSubmitInviteAction = @"performSubmitInviteAction";
@interface InviteSearch : RCTEventEmitter
@end
@interface InviteSearchController ()
@property (nonatomic, readonly) NSString* _Nonnull identifier;
@property (nonatomic, strong) NSMutableDictionary* _Nonnull items;
@property (nonatomic, nullable, weak) InviteSearch* module;
- (instancetype)initWithSearchModule:(InviteSearch *)module;
- (void)didReceiveResults:(NSArray<NSDictionary*> * _Nonnull)results
forQuery:(NSString * _Nonnull)query;
- (void)inviteDidSucceed;
- (void)inviteDidFailForItems:(NSArray<NSDictionary *> *)items;
@end
@implementation InviteSearch
static NSMutableDictionary* searchControllers;
RCT_EXTERN void RCTRegisterModule(Class);
+ (void)load {
RCTRegisterModule(self);
searchControllers = [[NSMutableDictionary alloc] init];
}
+ (NSString *)moduleName {
return @"InviteSearch";
}
- (NSArray<NSString *> *)supportedEvents {
return @[
InviteSearchPerformQueryAction,
InviteSearchPerformSubmitInviteAction
];
}
/**
* Calls the corresponding JitsiMeetView's delegate to request that the native
* invite search be presented.
*
* @param scope
*/
RCT_EXPORT_METHOD(launchNativeInvite:(NSString *)scope) {
// The JavaScript App needs to provide uniquely identifying information to
// the native module so that the latter may match the former to the native
// JitsiMeetView which hosts it.
JitsiMeetView *view = [JitsiMeetView viewForExternalAPIScope:scope];
if (!view) {
return;
}
id<JitsiMeetViewDelegate> delegate = view.delegate;
if (!delegate) {
return;
}
if ([delegate respondsToSelector:@selector(launchNativeInviteForSearchController:)]) {
InviteSearchController* searchController = [searchControllers objectForKey:scope];
if (!searchController) {
searchController = [self makeInviteSearchController];
}
[delegate launchNativeInviteForSearchController:searchController];
}
}
RCT_EXPORT_METHOD(inviteSucceeded:(NSString *)inviteScope) {
InviteSearchController* searchController = [searchControllers objectForKey:inviteScope];
[searchController inviteDidSucceed];
[searchControllers removeObjectForKey:inviteScope];
}
RCT_EXPORT_METHOD(inviteFailedForItems:(NSArray<NSDictionary *> *)items inviteScope:(NSString *)inviteScope) {
InviteSearchController* searchController = [searchControllers objectForKey:inviteScope];
[searchController inviteDidFailForItems:items];
}
RCT_EXPORT_METHOD(receivedResults:(NSArray *)results forQuery:(NSString *)query inviteScope:(NSString *)inviteScope) {
InviteSearchController* searchController = [searchControllers objectForKey:inviteScope];
[searchController didReceiveResults:results forQuery:query];
}
- (InviteSearchController *)makeInviteSearchController {
InviteSearchController* searchController = [[InviteSearchController alloc] initWithSearchModule:self];
[searchControllers setObject:searchController forKey:searchController.identifier];
return searchController;
}
- (void)performQuery:(NSString * _Nonnull)query inviteScope:(NSString * _Nonnull)inviteScope {
[self sendEventWithName:InviteSearchPerformQueryAction body:@{ @"query": query, @"inviteScope": inviteScope }];
}
- (void)cancelSearchForInviteScope:(NSString * _Nonnull)inviteScope {
[searchControllers removeObjectForKey:inviteScope];
}
- (void)submitSelectedItems:(NSArray<NSDictionary *> * _Nonnull)items inviteScope:(NSString * _Nonnull)inviteScope {
[self sendEventWithName:InviteSearchPerformSubmitInviteAction body:@{ @"selectedItems": items, @"inviteScope": inviteScope }];
}
@end
@implementation InviteSearchController
- (instancetype)initWithSearchModule:(InviteSearch *)module {
self = [super init];
if (self) {
_identifier = [[NSUUID UUID] UUIDString];
self.items = [[NSMutableDictionary alloc] init];
self.module = module;
}
return self;
}
- (void)performQuery:(NSString *)query {
[self.module performQuery:query inviteScope:self.identifier];
}
- (void)cancelSearch {
[self.module cancelSearchForInviteScope:self.identifier];
}
- (void)submitSelectedItemIds:(NSArray<NSString *> * _Nonnull)ids {
NSMutableArray* items = [[NSMutableArray alloc] init];
for (NSString* itemId in ids) {
id item = [self.items objectForKey:itemId];
if (item) {
[items addObject:item];
}
}
[self.module submitSelectedItems:items inviteScope:self.identifier];
}
- (void)didReceiveResults:(NSArray<NSDictionary *> *)results forQuery:(NSString *)query {
for (NSDictionary* item in results) {
NSString* itemId = item[@"id"];
NSString* itemType = item[@"type"];
if (itemId) {
[self.items setObject:item forKey:itemId];
} else if (itemType != nil && [itemType isEqualToString: @"phone"]) {
NSString* number = item[@"number"];
if (number) {
[self.items setObject:item forKey:number];
}
}
}
[self.delegate inviteSearchController:self didReceiveResults:results forQuery:query];
}
- (void)inviteDidSucceed {
[self.delegate inviteDidSucceedForSearchController:self];
}
- (void)inviteDidFailForItems:(NSArray<NSDictionary *> *)items {
if (!items) {
items = @[];
}
[self.delegate inviteDidFailForItems:items fromSearchController:self];
}
@end

View File

@@ -16,3 +16,4 @@
#import <JitsiMeet/JitsiMeetView.h>
#import <JitsiMeet/JitsiMeetViewDelegate.h>
#import <JitsiMeet/InviteSearch.h>

View File

@@ -21,10 +21,14 @@
@interface JitsiMeetView : UIView
@property (nonatomic, nullable, weak) id<JitsiMeetViewDelegate> delegate;
@property (nonatomic) BOOL addPeopleEnabled;
@property (copy, nonatomic, nullable) NSURL *defaultURL;
@property (nonatomic, nullable, weak) id<JitsiMeetViewDelegate> delegate;
@property (nonatomic) BOOL dialOutEnabled;
@property (nonatomic) BOOL pictureInPictureEnabled;
@property (nonatomic) BOOL welcomePageEnabled;

View File

@@ -268,6 +268,8 @@ static NSMapTable<NSString *, JitsiMeetView *> *views;
props[@"defaultURL"] = [self.defaultURL absoluteString];
}
props[@"addPeopleEnabled"] = @(self.addPeopleEnabled);
props[@"dialOutEnabled"] = @(self.dialOutEnabled);
props[@"externalAPIScope"] = externalAPIScope;
props[@"pictureInPictureEnabled"] = @(self.pictureInPictureEnabled);
props[@"welcomePageEnabled"] = @(self.welcomePageEnabled);

View File

@@ -14,6 +14,8 @@
* limitations under the License.
*/
@class InviteSearchController;
@protocol JitsiMeetViewDelegate <NSObject>
@optional
@@ -55,6 +57,15 @@
*/
- (void)conferenceWillLeave:(NSDictionary *)data;
/**
* Called when the invite button in the conference is tapped.
*
* The search controller provided can be used to query user search within the
* conference.
*/
- (void)launchNativeInviteForSearchController:(InviteSearchController *)searchController;
/**
* Called when entering Picture-in-Picture is requested by the user. The app
* should now activate its Picture-in-Picture implementation (and resize the

View File

@@ -1,97 +0,0 @@
/*
* 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.
*/
import Foundation
/// Coordinates the presentation of JitsiMeetViewController inside of
/// an external window that can be resized and dragged with custom PiP mode
open class JitsiMeetPresentationCoordinator: NSObject {
fileprivate let meetViewController: JitsiMeetViewController
fileprivate let meetWindow: PiPWindow
public init(meetViewController: JitsiMeetViewController? = nil,
meetWindow: PiPWindow? = nil) {
self.meetViewController = meetViewController ?? JitsiMeetViewController()
self.meetWindow = meetWindow ?? PiPWindow(frame: UIScreen.main.bounds)
super.init()
configureMeetWindow()
configureMeetViewController()
}
public func jitsiMeetView() -> JitsiMeetView {
return meetViewController.jitsiMeetView
}
open func show(completion: CompletionAction? = nil) {
meetWindow.show(completion: completion)
}
open func hide(completion: CompletionAction? = nil) {
meetWindow.hide(completion: completion)
}
open func cleanUp() {
// TODO: more clean up work on this
meetWindow.isHidden = true
meetWindow.stopDragGesture()
}
deinit {
cleanUp()
}
// MARK: - helpers
private func configureMeetViewController() {
meetViewController.jitsiMeetView.pictureInPictureEnabled = true
meetViewController.delegate = self
}
private func configureMeetWindow() {
meetWindow.backgroundColor = .clear
meetWindow.windowLevel = UIWindowLevelStatusBar + 100
meetWindow.rootViewController = self.meetViewController
}
}
extension JitsiMeetPresentationCoordinator: JitsiMeetViewControllerDelegate {
open func performPresentationUpdate(to: JitsiMeetPresentationUpdate) {
switch to {
case .enterPictureInPicture:
meetWindow.enterPictureInPicture()
case .traitChange:
// resize to full screen if rotation happens
if meetWindow.isInPiP {
meetWindow.exitPictureInPicture()
}
}
}
open func conferenceStarted() {
if meetWindow.isHidden {
meetWindow.show()
}
}
open func conferenceEnded(didFail: Bool) {
cleanUp()
}
}

View File

@@ -1,107 +0,0 @@
/*
* 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.
*/
public enum JitsiMeetPresentationUpdate {
/// The conference wants to enter Picture-in-Picture
case enterPictureInPicture
/// A system traitCollectionChange (usually screen rotation)
case traitChange
}
public protocol JitsiMeetViewControllerDelegate: class {
/// Notifies a change of the conference presentation style.
///
/// - Parameter to: The presentation state that will be changed to
func performPresentationUpdate(to: JitsiMeetPresentationUpdate)
/// The conference started
func conferenceStarted()
/// The conference ended
///
/// - Parameter didFail: The reason of ending the conference
func conferenceEnded(didFail: Bool)
}
/// Wrapper ViewController of a JitsiMeetView
///
/// Is suggested to override this class and implement some customization
/// on how to handle the JitsiMeetView delegate events
open class JitsiMeetViewController: UIViewController {
open weak var delegate: JitsiMeetViewControllerDelegate?
private(set) var jitsiMeetView: JitsiMeetView = JitsiMeetView()
override open func loadView() {
super.loadView()
self.view = jitsiMeetView
}
open override func viewDidLoad() {
super.viewDidLoad()
jitsiMeetView.delegate = self
}
open override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
delegate?.performPresentationUpdate(to: .traitChange)
}
}
extension JitsiMeetViewController: JitsiMeetViewDelegate {
open func conferenceWillJoin(_ data: [AnyHashable : Any]!) {
// do something
}
open func conferenceJoined(_ data: [AnyHashable : Any]!) {
DispatchQueue.main.async {
self.delegate?.conferenceStarted()
}
}
open func conferenceWillLeave(_ data: [AnyHashable : Any]!) {
// do something
}
open func conferenceLeft(_ data: [AnyHashable : Any]!) {
DispatchQueue.main.async {
self.delegate?.conferenceEnded(didFail: false)
}
}
open func conferenceFailed(_ data: [AnyHashable : Any]!) {
DispatchQueue.main.async {
self.delegate?.conferenceEnded(didFail: true)
}
}
open func loadConfigError(_ data: [AnyHashable : Any]!) {
DispatchQueue.main.async {
self.delegate?.conferenceEnded(didFail: true)
}
}
open func enterPicture(inPicture data: [AnyHashable : Any]!) {
DispatchQueue.main.async {
self.delegate?.performPresentationUpdate(to: .enterPictureInPicture)
}
}
}

View File

@@ -14,14 +14,15 @@
* limitations under the License.
*/
/// Alias defining a completion closure that returns a Bool
public typealias CompletionAction = (Bool) -> Void
public typealias AnimationCompletion = (Bool) -> Void
/// A window that allows its root view controller to be presented
/// in full screen or in a custom Picture in Picture mode
open class PiPWindow: UIWindow {
/// Coordinates the view state of a specified view to allow
/// to be presented in full screen or in a custom Picture in Picture mode.
/// This object will also provide the drag and tap interactions of the view
/// when is presented in Picure in Picture mode.
public class PiPViewCoordinator {
/// Limits the boundries of root view position on screen when minimized
/// Limits the boundries of view position on screen when minimized
public var dragBoundInsets: UIEdgeInsets = UIEdgeInsets(top: 25,
left: 5,
bottom: 5,
@@ -31,53 +32,75 @@ open class PiPWindow: UIWindow {
}
}
/// The size ratio for root view controller view when in PiP mode
public var pipSizeRatio: CGFloat = 0.333
/// The size ratio of the view when in PiP mode
public var pipSizeRatio: CGFloat = {
let deviceIdiom = UIScreen.main.traitCollection.userInterfaceIdiom
switch deviceIdiom {
case .pad:
return 0.25
case .phone:
return 0.33
default:
return 0.25
}
}()
/// The PiP state of this contents of the window
private(set) var isInPiP: Bool = false
private(set) var isInPiP: Bool = false // true if view is in PiP mode
private let dragController: DragGestureController = DragGestureController()
private(set) var view: UIView
private var currentBounds: CGRect = CGRect.zero
/// Used when in PiP mode to enable/disable exit PiP UI
private var tapGestureRecognizer: UITapGestureRecognizer?
private var exitPiPButton: UIButton?
/// Help out to bubble up the gesture detection outside of the rootVC frame
open override func point(inside point: CGPoint,
with event: UIEvent?) -> Bool {
guard let vc = rootViewController else {
return super.point(inside: point, with: event)
}
return vc.view.frame.contains(point)
private let dragController: DragGestureController = DragGestureController()
public init(withView view: UIView) {
self.view = view
}
/// animate in the window
open func show(completion: CompletionAction? = nil) {
if self.isHidden || self.alpha < 1 {
self.isHidden = false
self.alpha = 0
/// Configure the view to be always on top of all the contents
/// of the provided parent view.
/// If a parentView is not provided it will try to use the main window
public func configureAsStickyView(withParentView parentView: UIView? = nil) {
guard
let parentView = parentView ?? UIApplication.shared.keyWindow
else { return }
parentView.addSubview(view)
currentBounds = parentView.bounds
view.frame = currentBounds
view.layer.zPosition = CGFloat(Float.greatestFiniteMagnitude)
}
/// Show view with fade in animation
public func show(completion: AnimationCompletion? = nil) {
if view.isHidden || view.alpha < 1 {
view.isHidden = false
view.alpha = 0
animateTransition(animations: {
self.alpha = 1
animateTransition(animations: { [weak self] in
self?.view.alpha = 1
}, completion: completion)
}
}
/// animate out the window
open func hide(completion: CompletionAction? = nil) {
if !self.isHidden || self.alpha > 0 {
animateTransition(animations: {
self.alpha = 1
/// Hide view with fade out animation
public func hide(completion: AnimationCompletion? = nil) {
if view.isHidden || view.alpha > 0 {
animateTransition(animations: { [weak self] in
self?.view.alpha = 0
self?.view.isHidden = true
}, completion: completion)
}
}
/// Resize the root view to PiP mode
open func enterPictureInPicture() {
guard let view = rootViewController?.view else { return }
/// Resize view to and change state to custom PictureInPicture mode
/// This will resize view, add a gesture to enable user to "drag" view
/// around screen, and add a button of top of the view to be able to exit mode
public func enterPictureInPicture() {
isInPiP = true
animateRootViewChange()
animateViewChange()
dragController.startDragListener(inView: view)
dragController.insets = dragBoundInsets
@@ -89,10 +112,11 @@ open class PiPWindow: UIWindow {
view.addGestureRecognizer(tapGestureRecognizer)
}
/// Resize the root view to full screen
open func exitPictureInPicture() {
/// Exit Picture in picture mode, this will resize view, remove
/// exit pip button, and disable the drag gesture
@objc public func exitPictureInPicture() {
isInPiP = false
animateRootViewChange()
animateViewChange()
dragController.stopDragListener()
// hide PiP UI
@@ -105,6 +129,13 @@ open class PiPWindow: UIWindow {
tapGestureRecognizer = nil
}
/// Reset view to provide bounds, use this method on rotation or
/// screen size changes
public func resetBounds(bounds: CGRect) {
currentBounds = bounds
exitPictureInPicture()
}
/// Stop the dragging gesture of the root view
public func stopDragGesture() {
dragController.stopDragListener()
@@ -122,41 +153,14 @@ open class PiPWindow: UIWindow {
button.backgroundColor = .gray
button.layer.cornerRadius = size.width / 2
button.frame = CGRect(origin: CGPoint.zero, size: size)
if let view = rootViewController?.view {
button.center = view.convert(view.center, from:view.superview)
}
button.center = view.convert(view.center, from: view.superview)
button.addTarget(target, action: action, for: .touchUpInside)
return button
}
// MARK: - Manage presentation switching
private func animateRootViewChange() {
UIView.animate(withDuration: 0.25) {
self.rootViewController?.view.frame = self.changeRootViewRect()
self.rootViewController?.view.setNeedsLayout()
}
}
private func changeRootViewRect() -> CGRect {
guard isInPiP else {
return self.bounds
}
// resize to suggested ratio and position to the bottom right
let adjustedBounds = UIEdgeInsetsInsetRect(self.bounds, dragBoundInsets)
let size = CGSize(width: bounds.size.width * pipSizeRatio,
height: bounds.size.height * pipSizeRatio)
let x: CGFloat = adjustedBounds.maxX - size.width
let y: CGFloat = adjustedBounds.maxY - size.height
return CGRect(x: x, y: y, width: size.width, height: size.height)
}
// MARK: - Exit PiP
// MARK: - Interactions
@objc private func toggleExitPiP() {
guard let view = rootViewController?.view else { return }
if exitPiPButton == nil {
// show button
let exitSelector = #selector(exitPictureInPicture)
@@ -172,18 +176,40 @@ open class PiPWindow: UIWindow {
}
}
@objc private func exitPiP() {
exitPictureInPicture()
// MARK: - Size calculation
private func animateViewChange() {
UIView.animate(withDuration: 0.25) {
self.view.frame = self.changeViewRect()
self.view.setNeedsLayout()
}
}
// MARK: - Animation transition
private func changeViewRect() -> CGRect {
let bounds = currentBounds
guard isInPiP else {
return bounds
}
// resize to suggested ratio and position to the bottom right
let adjustedBounds = UIEdgeInsetsInsetRect(bounds, dragBoundInsets)
let size = CGSize(width: bounds.size.width * pipSizeRatio,
height: bounds.size.height * pipSizeRatio)
let x: CGFloat = adjustedBounds.maxX - size.width
let y: CGFloat = adjustedBounds.maxY - size.height
return CGRect(x: x, y: y, width: size.width, height: size.height)
}
// MARK: - Animation helpers
private func animateTransition(animations: @escaping () -> Void,
completion: CompletionAction?) {
completion: AnimationCompletion?) {
UIView.animate(withDuration: 0.1,
delay: 0,
options: .beginFromCurrentState,
animations: animations,
completion: completion)
}
}

20
lang/languages-ko.json Normal file
View File

@@ -0,0 +1,20 @@
{
"en": "",
"bg": "",
"de": "",
"es": "",
"fr": "",
"hy": "",
"it": "",
"oc": "",
"pl": "",
"ptBR": "",
"ru": "",
"sk": "",
"sl": "",
"sv": "",
"tr": "",
"zhCN": "",
"nb": "",
"eo": ""
}

512
lang/main-ko.json Normal file
View File

@@ -0,0 +1,512 @@
{
"contactlist": "",
"contactlist_plural_undefined": "",
"passwordSetRemotely": "",
"poweredby": "",
"inviteUrlDefaultMsg": "",
"me": "",
"speaker": "",
"raisedHand": "",
"defaultNickname": "",
"defaultLink": "",
"audioDevices": {
"bluetooth": "",
"headphones": "",
"phone": "",
"speaker": ""
},
"audioOnly": {
"audioOnly": "",
"featureToggleDisabled": ""
},
"userMedia": {
"react-nativeGrantPermissions": "",
"chromeGrantPermissions": "",
"androidGrantPermissions": "",
"firefoxGrantPermissions": "",
"operaGrantPermissions": "",
"iexplorerGrantPermissions": "",
"safariGrantPermissions": "",
"nwjsGrantPermissions": "",
"edgeGrantPermissions": ""
},
"keyboardShortcuts": {
"keyboardShortcuts": "",
"raiseHand": "",
"pushToTalk": "",
"toggleScreensharing": "",
"toggleFilmstrip": "",
"toggleShortcuts": "",
"focusLocal": "",
"focusRemote": "",
"toggleChat": "",
"mute": "",
"fullScreen": "",
"videoMute": "",
"showSpeakerStats": ""
},
"welcomepage": {
"appDescription": "",
"audioVideoSwitch": {
"audio": "",
"video": ""
},
"calendar": "",
"go": "",
"join": "",
"privacy": "",
"roomname": "",
"roomnameHint": "",
"sendFeedback": "",
"terms": "",
"title": ""
},
"startupoverlay": {
"policyText": "",
"title": ""
},
"suspendedoverlay": {
"title": "",
"text": "",
"rejoinKeyTitle": ""
},
"toolbar": {
"addPeople": "",
"audioonly": "",
"mute": "",
"videomute": "",
"authenticate": "",
"lock": "",
"chat": "",
"etherpad": "",
"sharedvideo": "",
"sharescreen": "",
"fullscreen": "",
"sip": "",
"Settings": "",
"hangup": "",
"login": "",
"logout": "",
"dialpad": "",
"sharedVideoMutedPopup": "",
"micMutedPopup": "",
"talkWhileMutedPopup": "",
"unableToUnmutePopup": "",
"cameraDisabled": "",
"micDisabled": "",
"filmstrip": "",
"profile": "",
"raiseHand": ""
},
"unsupportedBrowser": {
"appNotInstalled": "",
"downloadApp": "",
"openApp": ""
},
"bottomtoolbar": {
"chat": "",
"filmstrip": "",
"contactlist": ""
},
"chat": {
"nickname": {
"title": "",
"popover": ""
},
"messagebox": ""
},
"settings": {
"title": "",
"update": "",
"name": "",
"startAudioMuted": "",
"startVideoMuted": "",
"selectCamera": "",
"selectMic": "",
"selectAudioOutput": "",
"followMe": "",
"noDevice": "",
"cameraAndMic": "",
"moderator": "",
"password": "",
"audioVideo": ""
},
"profile": {
"title": "",
"setDisplayNameLabel": "",
"setEmailLabel": "",
"setEmailInput": ""
},
"videothumbnail": {
"moderator": "",
"videomute": "",
"mute": "",
"kick": "",
"muted": "",
"domute": "",
"flip": "",
"remoteControl": ""
},
"connectionindicator": {
"header": "",
"bitrate": "",
"packetloss": "",
"resolution": "",
"framerate": "",
"less": "",
"more": "",
"address": "",
"remoteport": "",
"remoteport_plural_undefined": "",
"localport": "",
"localport_plural_undefined": "",
"localaddress": "",
"localaddress_plural_undefined": "",
"remoteaddress": "",
"remoteaddress_plural_undefined": "",
"transport": "",
"transport_plural_undefined": "",
"bandwidth": "",
"na": "",
"turn": "",
"quality": {
"good": "",
"inactive": "",
"lost": "",
"nonoptimal": "",
"poor": ""
},
"status": ""
},
"notify": {
"disconnected": "",
"moderator": "",
"connectedOneMember": "",
"connectedTwoMembers": "",
"connectedThreePlusMembers": "",
"somebody": "",
"me": "",
"focus": "",
"focusFail": "",
"grantedTo": "",
"grantedToUnknown": "",
"muted": "",
"mutedTitle": "",
"raisedHand": "",
"suboptimalExperienceTitle": "",
"suboptimalExperienceDescription": ""
},
"dialog": {
"allow": "",
"kickMessage": "",
"popupErrorTitle": "",
"popupError": "",
"passwordErrorTitle": "",
"passwordError": "",
"passwordError2": "",
"connectError": "",
"connectErrorWithMsg": "",
"incorrectPassword": "",
"connecting": "",
"copy": "",
"contactSupport": "",
"error": "",
"detectext": "",
"failedpermissions": "",
"conferenceReloadTitle": "",
"conferenceReloadMsg": "",
"conferenceDisconnectTitle": "",
"conferenceDisconnectMsg": "",
"dismiss": "",
"rejoinNow": "",
"maxUsersLimitReachedTitle": "",
"maxUsersLimitReached": "",
"lockTitle": "",
"lockMessage": "",
"warning": "",
"passwordNotSupportedTitle": "",
"passwordNotSupported": "",
"internalErrorTitle": "",
"internalError": "",
"unableToSwitch": "",
"SLDFailure": "",
"SRDFailure": "",
"oops": "",
"currentPassword": "",
"passwordLabel": "",
"defaultError": "",
"passwordRequired": "",
"Ok": "",
"done": "",
"Remove": "",
"removePassword": "",
"shareVideoTitle": "",
"shareVideoLinkError": "",
"removeSharedVideoTitle": "",
"removeSharedVideoMsg": "",
"alreadySharedVideoMsg": "",
"alreadySharedVideoTitle": "",
"WaitingForHost": "",
"WaitForHostMsg": "",
"IamHost": "",
"Cancel": "",
"Submit": "",
"retry": "",
"logoutTitle": "",
"logoutQuestion": "",
"sessTerminated": "",
"hungUp": "",
"joinAgain": "",
"Share": "",
"Save": "",
"recording": "",
"recordingToken": "",
"Back": "",
"serviceUnavailable": "",
"gracefulShutdown": "",
"Yes": "",
"reservationError": "",
"reservationErrorMsg": "",
"password": "",
"userPassword": "",
"token": "",
"tokenAuthFailedTitle": "",
"tokenAuthFailed": "",
"displayNameRequired": "",
"enterDisplayName": "",
"feedbackHelp": "",
"feedbackQuestion": "",
"thankYou": "",
"sorryFeedback": "",
"liveStreaming": "",
"streamKey": "",
"startLiveStreaming": "",
"stopStreamingWarning": "",
"stopRecordingWarning": "",
"stopLiveStreaming": "",
"stopRecording": "",
"doNotShowMessageAgain": "",
"permissionDenied": "",
"screenSharingFailedToInstall": "",
"screenSharingFailedToInstallTitle": "",
"screenSharingFirefoxPermissionDeniedError": "",
"screenSharingFirefoxPermissionDeniedTitle": "",
"screenSharingPermissionDeniedError": "",
"cameraUnsupportedResolutionError": "",
"cameraUnknownError": "",
"cameraPermissionDeniedError": "",
"cameraNotFoundError": "",
"cameraConstraintFailedError": "",
"micUnknownError": "",
"micPermissionDeniedError": "",
"micNotFoundError": "",
"micConstraintFailedError": "",
"micNotSendingDataTitle": "",
"micNotSendingData": "",
"cameraNotSendingDataTitle": "",
"cameraNotSendingData": "",
"goToStore": "",
"externalInstallationTitle": "",
"externalInstallationMsg": "",
"inlineInstallationMsg": "",
"inlineInstallExtension": "",
"muteParticipantTitle": "",
"muteParticipantBody": "",
"muteParticipantButton": "",
"remoteControlTitle": "",
"remoteControlRequestMessage": "",
"remoteControlShareScreenWarning": "",
"remoteControlDeniedMessage": "",
"remoteControlAllowedMessage": "",
"remoteControlErrorMessage": "",
"startRemoteControlErrorMessage": "",
"remoteControlStopMessage": "",
"close": "",
"shareYourScreen": "",
"yourEntireScreen": "",
"applicationWindow": ""
},
"email": {
"sharedKey": "",
"subject": "",
"body": "",
"and": ""
},
"connection": {
"ERROR": "",
"CONNECTING": "",
"RECONNECTING": "",
"CONNFAIL": "",
"AUTHENTICATING": "",
"AUTHFAIL": "",
"CONNECTED": "",
"DISCONNECTED": "",
"DISCONNECTING": "",
"ATTACHED": ""
},
"recording": {
"busy": "",
"busyTitle": "",
"buttonTooltip": "",
"error": "",
"failedToStart": "",
"off": "",
"on": "",
"pending": "",
"serviceName": "",
"unavailable": "",
"unavailableTitle": ""
},
"liveStreaming": {
"busy": "",
"busyTitle": "",
"buttonTooltip": "",
"changeSignIn": "",
"choose": "",
"chooseCTA": "",
"enterStreamKey": "",
"error": "",
"errorAPI": "",
"failedToStart": "",
"off": "",
"on": "",
"pending": "",
"serviceName": "",
"signIn": "",
"signInCTA": "",
"start": "",
"streamIdHelp": "",
"unavailableTitle": ""
},
"videoSIPGW": {
"busy": "",
"busyTitle": "",
"errorInvite": "",
"errorInviteTitle": "",
"errorAlreadyInvited": "",
"errorInviteFailedTitle": "",
"errorInviteFailed": "",
"pending": "",
"serviceName": "",
"unavailableTitle": ""
},
"speakerStats": {
"hours": "",
"minutes": "",
"name": "",
"seconds": "",
"speakerStats": "",
"speakerTime": ""
},
"deviceSelection": {
"deviceSettings": "",
"noPermission": "",
"previewUnavailable": "",
"selectADevice": "",
"testAudio": ""
},
"videoStatus": {
"callQuality": "",
"hd": "",
"highDefinition": "",
"labelTooltipVideo": "",
"labelTooltipAudioOnly": "",
"ld": "",
"lowDefinition": "",
"onlyAudioAvailable": "",
"onlyAudioSupported": "",
"p2pEnabled": "",
"p2pVideoQualityDescription": "",
"recHighDefinitionOnly": "",
"sd": "",
"standardDefinition": "",
"qualityButtonTip": ""
},
"dialOut": {
"statusMessage": ""
},
"addPeople": {
"add": "",
"countryNotSupported": "",
"countryReminder": "",
"disabled": "",
"invite": "",
"loading": "",
"loadingNumber": "",
"loadingPeople": "",
"noResults": "",
"noValidNumbers": "",
"searchNumbers": "",
"searchPeople": "",
"searchPeopleAndNumbers": "",
"telephone": "",
"title": "",
"failedToAdd": ""
},
"inlineDialogFailure": {
"msg": "",
"retry": "",
"support": "",
"supportMsg": ""
},
"deviceError": {
"cameraError": "",
"microphoneError": "",
"cameraPermission": "",
"microphonePermission": ""
},
"feedback": {
"average": "",
"bad": "",
"good": "",
"rateExperience": "",
"veryBad": "",
"veryGood": ""
},
"info": {
"addPassword": "",
"cancelPassword": "",
"conferenceURL": "",
"country": "",
"dialANumber": "",
"dialInNumber": "",
"dialInConferenceID": "",
"dialInNotSupported": "",
"genericError": "",
"invitePhone": "",
"invitePhoneAlternatives": "",
"inviteURL": "",
"moreNumbers": "",
"noNumbers": "",
"noPassword": "",
"noRoom": "",
"numbers": "",
"password": "",
"title": "",
"tooltip": ""
},
"settingsView": {
"alertOk": "",
"alertTitle": "",
"alertURLText": "",
"conferenceSection": "",
"displayName": "",
"email": "",
"header": "",
"profileSection": "",
"serverURL": "",
"startWithAudioMuted": "",
"startWithVideoMuted": ""
},
"calendarSync": {
"later": "",
"next": "",
"nextMeeting": "",
"now": ""
},
"recentList": {
"today": "",
"yesterday": "",
"earlier": ""
}
}

View File

@@ -1,5 +1,4 @@
{
"contactlist": "__count__ Member",
"contactlist_plural": "__count__ Members",
"passwordSetRemotely": "set by another member",
"poweredby": "powered by",
@@ -23,6 +22,7 @@
"react-nativeGrantPermissions": "Select <b><i>Allow</i></b> when your browser asks for permissions.",
"chromeGrantPermissions": "Select <b><i>Allow</i></b> when your browser asks for permissions.",
"androidGrantPermissions": "Select <b><i>Allow</i></b> when your browser asks for permissions.",
"electronGrantPermissions": "Please grant permissions to use your camera and microphone",
"firefoxGrantPermissions": "Select <b><i>Share Selected Device</i></b> when your browser asks for permissions.",
"operaGrantPermissions": "Select <b><i>Allow</i></b> when your browser asks for permissions.",
"iexplorerGrantPermissions": "Select <b><i>OK</i></b> when your browser asks for permissions.",
@@ -73,21 +73,28 @@
"toolbar": {
"addPeople": "Add people to your call",
"audioonly": "Enable / Disable audio only mode (saves bandwidth)",
"callQuality": "Manage call quality",
"enterFullScreen": "View full screen",
"exitFullScreen": "Exit full screen",
"feedback": "Leave feedback",
"moreActions": "More actions",
"mute": "Mute / Unmute",
"videomute": "Start / Stop camera",
"authenticate": "Authenticate",
"lock": "Lock / Unlock room",
"chat": "Open / Close chat",
"etherpad": "Open / Close shared document",
"documentOpen": "Open shared document",
"documentClose": "Close shared document",
"sharedvideo": "Share a YouTube video",
"sharescreen": "Start / Stop screen sharing",
"sharescreen": "Screen share",
"stopSharedVideo": "Stop YouTube video",
"fullscreen": "View / Exit full screen",
"sip": "Call SIP number",
"Settings": "Settings",
"hangup": "Leave",
"login": "Login",
"logout": "Logout",
"dialpad": "Open / Close dialpad",
"sharedVideoMutedPopup": "Your shared video has been muted so that you can talk to the other members.",
"micMutedPopup": "Your microphone has been muted so that you would fully enjoy your shared video.",
"talkWhileMutedPopup": "Trying to speak? You are muted.",
@@ -96,17 +103,9 @@
"micDisabled": "Microphone is not available",
"filmstrip": "Show / Hide videos",
"profile": "Edit your profile",
"raiseHand": "Raise / Lower your hand"
},
"unsupportedBrowser": {
"appNotInstalled": "Join this meeting with __app__ on your phone.",
"downloadApp": "Download the app",
"openApp": "Continue to __app__"
},
"bottomtoolbar": {
"chat": "Open / close chat",
"filmstrip": "Show / hide videos",
"contactlist": "View and invite members"
"raiseHand": "Raise / Lower your hand",
"shortcuts": "View shortcuts",
"speakerStats": "Speaker stats"
},
"chat":{
"nickname": {
@@ -285,6 +284,7 @@
"liveStreaming": "Live Streaming",
"streamKey": "Live stream key",
"startLiveStreaming": "Go live now",
"startRecording": "Start recording",
"stopStreamingWarning": "Are you sure you would like to stop the live streaming?",
"stopRecordingWarning": "Are you sure you would like to stop the recording?",
"stopLiveStreaming": "Stop live streaming",
@@ -445,10 +445,13 @@
"videoStatus": {
"callQuality": "Call Quality",
"hd": "HD",
"hdTooltip": "Viewing high definition video",
"highDefinition": "High definition",
"labelTooltipVideo": "Current video quality",
"labelTooltipAudioOnly": "Audio-only mode enabled",
"labelTooiltipNoVideo": "No video",
"labelTooltipVideo": "Current video quality",
"ld": "LD",
"ldTooltip": "Viewing low definition video",
"lowDefinition": "Low definition",
"onlyAudioAvailable": "Only audio is available",
"onlyAudioSupported": "We only support audio in this browser.",
@@ -456,6 +459,7 @@
"p2pVideoQualityDescription": "In peer to peer mode, received call quality can only be toggled between high and audio only. Other settings will not be honored until peer to peer is exited.",
"recHighDefinitionOnly": "Will prefer high definition.",
"sd": "SD",
"sdTooltip": "Viewing standard definition video",
"standardDefinition": "Standard definition",
"qualityButtonTip": "Change received video quality"
},
@@ -463,7 +467,7 @@
"statusMessage": "is now __status__"
},
"addPeople": {
"add": "Add",
"add": "Invite",
"countryNotSupported": "We do not support this destination yet.",
"countryReminder": "Calling outside the US? Please make sure you start with the country code!",
"disabled": "You can't invite people.",
@@ -473,11 +477,12 @@
"loadingPeople": "Searching for people to invite",
"noResults": "No matching search results",
"noValidNumbers": "Please enter a phone number",
"searchNumbers": "Enter a phone number to invite",
"searchPeople": "Enter a name to invite",
"searchPeopleAndNumbers": "Enter a name or phone number to invite",
"notAvailable": "You can't invite people.",
"searchNumbers": "Add phone numbers",
"searchPeople": "Search for people",
"searchPeopleAndNumbers": "Search for people or add their phone numbers",
"telephone": "Telephone: __number__",
"title": "Invite people to your meeting",
"title": "Invite people to this meeting",
"failedToAdd": "Failed to add members"
},
"inlineDialogFailure": {
@@ -496,30 +501,33 @@
"average": "Average",
"bad": "Bad",
"good": "Good",
"rateExperience": "Please rate your meeting experience.",
"detailsLabel": "Tell us more about it.",
"rateExperience": "Rate your meeting experience",
"veryBad": "Very Bad",
"veryGood": "Very Good"
},
"info": {
"addPassword": "Add password",
"cancelPassword": "Cancel password",
"conferenceURL": "Link: __url__",
"conferenceURL": "Link:",
"country": "Country",
"dialANumber": "To join your meeting, dial one of these numbers and then enter this PIN: __conferenceID__#",
"dialInNumber": "Dial-in: __phoneNumber__",
"dialInConferenceID": "PIN: __conferenceID__#",
"dialInNumber": "Dial-in:",
"dialInConferenceID": "PIN:",
"dialInNotSupported": "Sorry, dialing in is currently not suppported.",
"genericError": "Whoops, something went wrong.",
"inviteLiveStream": "To view the live stream of this meeting, click this link: __url__",
"invitePhone": "To join by phone, dial __number__ and enter this PIN: __conferenceID__#",
"invitePhoneAlternatives": "To view more phone numbers, click this link: __url__",
"inviteURL": "To join the video meeting, click this link: __url__",
"liveStreamURL": "Live stream:",
"moreNumbers": "More numbers",
"noNumbers": "No dial-in numbers.",
"noPassword": "None",
"noRoom": "No room was specified to dial-in into.",
"numbers": "Dial-in Numbers",
"password": "Password:",
"title": "Call info",
"title": "Share",
"tooltip": "Get access info about the meeting"
},
"settingsView": {
@@ -539,11 +547,25 @@
"later": "Later",
"next": "Upcoming",
"nextMeeting": "next meeting",
"now": "Now"
"now": "Now",
"permissionButton": "Open settings",
"permissionMessage": "Calendar permission is required to list your meetings in the app."
},
"recentList": {
"today": "Today",
"yesterday": "Yesterday",
"earlier": "Earlier"
},
"sectionList": {
"pullToRefresh": "Pull to refresh"
},
"deepLinking": {
"title": "Launching your meeting in __app__...",
"description": "Nothing happened? We tried launching your meeting in the __app__ desktop app. Try again or launch it in the __app__ web app.",
"tryAgainButton": "Try again in desktop",
"launchWebButton": "Launch in web",
"appNotInstalled": "You need the __app__ mobile app to join this meeting on your phone.",
"downloadApp": "Download the app",
"openApp": "Continue to the app"
}
}

View File

@@ -81,10 +81,6 @@ function initCommands() {
sendAnalytics(createApiEvent('chat.toggled'));
APP.UI.toggleChat();
},
'toggle-contact-list': () => {
sendAnalytics(createApiEvent('contact.list.toggled'));
APP.UI.toggleContactList();
},
'toggle-share-screen': () => {
sendAnalytics(createApiEvent('screen.sharing.toggled'));
toggleScreenSharing();

View File

@@ -24,7 +24,6 @@ const commands = {
submitFeedback: 'submit-feedback',
toggleAudio: 'toggle-audio',
toggleChat: 'toggle-chat',
toggleContactList: 'toggle-contact-list',
toggleFilmStrip: 'toggle-film-strip',
toggleShareScreen: 'toggle-share-screen',
toggleVideo: 'toggle-video'
@@ -551,7 +550,6 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
* toggleVideo - mutes / unmutes video. no arguments
* toggleFilmStrip - hides / shows the filmstrip. no arguments
* toggleChat - hides / shows chat. no arguments.
* toggleContactList - hides / shows contact list. no arguments.
* toggleShareScreen - starts / stops screen sharing. no arguments.
*
* @param {Object} commandList - The object with commands to be executed.

View File

@@ -30,19 +30,13 @@ import {
} from '../../react/features/base/participants';
import { destroyLocalTracks } from '../../react/features/base/tracks';
import { openDisplayNamePrompt } from '../../react/features/display-name';
import { setEtherpadHasInitialzied } from '../../react/features/etherpad';
import {
setNotificationsEnabled,
showWarningNotification
} from '../../react/features/notifications';
import {
checkAutoEnableDesktopSharing,
clearButtonPopup,
dockToolbox,
setButtonPopupTimeout,
setToolbarButton,
showDialPadButton,
showEtherpadButton,
showSharedVideoButton,
showToolbox
} from '../../react/features/toolbox';
@@ -100,9 +94,6 @@ const UIListeners = new Map([
], [
UIEvents.SHARED_VIDEO_CLICKED,
() => sharedVideoManager && sharedVideoManager.toggleSharedVideo()
], [
UIEvents.TOGGLE_FULLSCREEN,
() => UI.toggleFullScreen()
], [
UIEvents.TOGGLE_CHAT,
() => UI.toggleChat()
@@ -120,9 +111,6 @@ const UIListeners = new Map([
UI.toggleSidePanel('settings_container');
}
}
], [
UIEvents.TOGGLE_CONTACT_LIST,
() => UI.toggleContactList()
], [
UIEvents.TOGGLE_PROFILE,
() => UI.toggleSidePanel('profile_container')
@@ -135,14 +123,6 @@ const UIListeners = new Map([
]
]);
/**
* Toggles the application in and out of full screen mode
* (a.k.a. presentation mode in Chrome).
*/
UI.toggleFullScreen = function() {
UIUtil.isFullScreen() ? UIUtil.exitFullScreen() : UIUtil.enterFullScreen();
};
/**
* Indicates if we're currently in full screen mode.
*
@@ -255,12 +235,20 @@ UI.showLocalConnectionInterrupted = function(isInterrupted) {
/**
* Sets the "raised hand" status for a participant.
*
* @param {string} id - The id of the participant whose raised hand UI should
* be updated.
* @param {string} name - The name of the participant with the raised hand
* update.
* @param {boolean} raisedHandStatus - Whether the participant's hand is raised
* or not.
* @returns {void}
*/
UI.setRaisedHandStatus = (participant, raisedHandStatus) => {
VideoLayout.setRaisedHandStatus(participant.getId(), raisedHandStatus);
UI.setRaisedHandStatus = (id, name, raisedHandStatus) => {
VideoLayout.setRaisedHandStatus(id, raisedHandStatus);
if (raisedHandStatus) {
messageHandler.participantNotification(
participant.getDisplayName(),
name,
'notify.somebody',
'connected',
'notify.raisedHand');
@@ -280,7 +268,7 @@ UI.setLocalRaisedHandStatus
* Initialize conference UI.
*/
UI.initConference = function() {
const { dispatch, getState } = APP.store;
const { getState } = APP.store;
const { email, id, name } = getLocalParticipant(getState);
// Update default button states before showing the toolbar
@@ -300,8 +288,6 @@ UI.initConference = function() {
UI.setUserEmail(id, email);
}
dispatch(checkAutoEnableDesktopSharing());
// 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
@@ -354,25 +340,28 @@ UI.start = function() {
VideoLayout.resizeVideoArea(true, true);
sharedVideoManager = new SharedVideoManager(eventEmitter);
// eslint-disable-next-line no-negated-condition
if (!interfaceConfig.filmStripOnly) {
if (interfaceConfig.filmStripOnly) {
$('body').addClass('filmstrip-only');
Filmstrip.setFilmstripOnly();
APP.store.dispatch(setNotificationsEnabled(false));
} else {
// Initialise the recording module.
if (config.enableRecording) {
Recording.init(eventEmitter, config.recordingType);
}
config.enableRecording
&& Recording.init(eventEmitter, config.recordingType);
// Initialize side panels
SidePanels.init(eventEmitter);
} else {
$('body').addClass('filmstrip-only');
UI.showToolbar();
Filmstrip.setFilmstripOnly();
APP.store.dispatch(setNotificationsEnabled(false));
// 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');
}
if (interfaceConfig.VERTICAL_FILMSTRIP) {
$('body').addClass('vertical-filmstrip');
}
interfaceConfig.VERTICAL_FILMSTRIP
&& $('body').addClass('vertical-filmstrip');
document.title = interfaceConfig.APP_NAME;
};
@@ -404,12 +393,7 @@ UI.bindEvents = () => {
// Resize and reposition videos in full screen mode.
$(document).on(
'webkitfullscreenchange mozfullscreenchange fullscreenchange',
() => {
eventEmitter.emit(
UIEvents.FULLSCREEN_TOGGLED,
UIUtil.isFullScreen());
onResize();
});
onResize);
$(window).resize(onResize);
};
@@ -456,12 +440,6 @@ UI.addRemoteStream = track => VideoLayout.onRemoteStreamAdded(track);
*/
UI.removeRemoteStream = track => VideoLayout.onRemoteStreamRemoved(track);
/**
* Update chat subject.
* @param {string} subject new chat subject
*/
UI.setSubject = subject => Chat.setSubject(subject);
/**
* Setup and show Etherpad.
* @param {string} name etherpad id
@@ -474,7 +452,7 @@ UI.initEtherpad = name => {
etherpadManager
= new EtherpadManager(config.etherpad_base, name, eventEmitter);
APP.store.dispatch(showEtherpadButton());
APP.store.dispatch(setEtherpadHasInitialzied());
};
/**
@@ -542,10 +520,6 @@ UI.onPeerVideoTypeChanged
UI.updateLocalRole = isModerator => {
VideoLayout.showModeratorIndicator();
APP.store.dispatch(showSharedVideoButton());
Recording.showRecordingButton(isModerator);
if (isModerator) {
if (!interfaceConfig.DISABLE_FOCUS_INDICATOR) {
messageHandler.participantNotification(
@@ -639,11 +613,6 @@ UI.isChatVisible = () => Chat.isVisible();
*/
UI.toggleChat = () => UI.toggleSidePanel('chat_container');
/**
* Toggles contact list panel.
*/
UI.toggleContactList = () => UI.toggleSidePanel('contacts_container');
/**
* Toggles the given side panel.
*
@@ -659,27 +628,6 @@ UI.inputDisplayNameHandler = function(newDisplayName) {
eventEmitter.emit(UIEvents.NICKNAME_CHANGED, newDisplayName);
};
/**
* Show custom popup/tooltip for a specified button.
*
* @param {string} buttonName - The name of the button as specified in the
* button configurations for the toolbar.
* @param {string} popupSelectorID - The id of the popup to show as specified in
* the button configurations for the toolbar.
* @param {boolean} show - True or false/show or hide the popup
* @param {number} timeout - The time to show the popup
* @returns {void}
*/
// eslint-disable-next-line max-params
UI.showCustomToolbarPopup = function(buttonName, popupID, show, timeout) {
const action
= show
? setButtonPopupTimeout(buttonName, popupID, timeout)
: clearButtonPopup(buttonName);
APP.store.dispatch(action);
};
/**
* Return the type of the remote video.
* @param jid the jid for the remote video
@@ -902,17 +850,6 @@ UI.promptDisplayName = () => {
*/
UI.setAudioLevel = (id, lvl) => VideoLayout.setAudioLevel(id, lvl);
/**
* Update state of desktop sharing buttons.
*
* @returns {void}
*/
UI.updateDesktopSharingButtons
= () =>
APP.store.dispatch(setToolbarButton('desktop', {
toggled: APP.conference.isSharingScreen
}));
/**
* Hide connection quality statistics from UI.
*/
@@ -944,9 +881,6 @@ UI.addMessage = function(from, displayName, message, stamp) {
Chat.updateChatConversation(from, displayName, message, stamp);
};
UI.updateDTMFSupport
= isDTMFSupported => APP.store.dispatch(showDialPadButton(isDTMFSupported));
UI.updateRecordingState = function(state) {
Recording.updateRecordingState(state);
};

View File

@@ -1,4 +1,7 @@
/* global $, interfaceConfig */
/* global $, APP, interfaceConfig */
import { setDocumentEditingState } from '../../../react/features/etherpad';
import { getToolboxHeight } from '../../../react/features/toolbox';
import VideoLayout from '../videolayout/VideoLayout';
import LargeContainer from '../videolayout/LargeContainer';
@@ -126,7 +129,7 @@ class Etherpad extends LargeContainer {
let height, width;
if (interfaceConfig.VERTICAL_FILMSTRIP) {
height = containerHeight;
height = containerHeight - getToolboxHeight();
width = containerWidth - Filmstrip.getFilmstripWidth();
} else {
height = containerHeight - Filmstrip.getFilmstripHeight();
@@ -152,6 +155,9 @@ class Etherpad extends LargeContainer {
document.body.style.background = '#eeeeee';
$iframe.css({ visibility: 'visible' });
$container.css({ zIndex: 2 });
APP.store.dispatch(setDocumentEditingState(true));
resolve();
});
});
@@ -170,6 +176,9 @@ class Etherpad extends LargeContainer {
$iframe.fadeOut(300, () => {
$iframe.css({ visibility: 'hidden' });
$container.css({ zIndex: 0 });
APP.store.dispatch(setDocumentEditingState(false));
resolve();
});
});
@@ -242,5 +251,7 @@ export default class EtherpadManager {
this.eventEmitter
.emit(UIEvents.TOGGLED_SHARED_DOCUMENT, !isVisible);
APP.store.dispatch(setDocumentEditingState(!isVisible));
}
}

View File

@@ -1,4 +1,4 @@
/* global APP, $, config, interfaceConfig */
/* global APP, config, interfaceConfig */
/*
* Copyright @ 2015 Atlassian Pty Ltd
*
@@ -35,6 +35,7 @@ import {
StartLiveStreamDialog,
StopLiveStreamDialog,
hideRecordingLabel,
setRecordingType,
updateRecordingState
} from '../../../react/features/recording';
@@ -108,7 +109,10 @@ function _requestLiveStreamId() {
return new Promise((resolve, reject) =>
APP.store.dispatch(openDialog(StartLiveStreamDialog, {
onCancel: reject,
onSubmit: resolve
onSubmit: (streamId, broadcastId) => resolve({
broadcastId,
streamId
})
})));
}
@@ -191,8 +195,7 @@ function isStartingStatus(status) {
/**
* Manages the recording user interface and user experience.
* @type {{init, initRecordingButton, showRecordingButton, updateRecordingState,
* updateRecordingUI, checkAutoRecord}}
* @type {{init, updateRecordingState, updateRecordingUI, checkAutoRecord}}
*/
const Recording = {
/**
@@ -202,6 +205,8 @@ const Recording = {
this.eventEmitter = eventEmitter;
this.recordingType = recordingType;
APP.store.dispatch(setRecordingType(recordingType));
this.updateRecordingState(APP.conference.getRecordingState());
if (recordingType === 'jibri') {
@@ -212,12 +217,8 @@ const Recording = {
Object.assign(this, RECORDING_TRANSLATION_KEYS);
}
// XXX Due to the React-ification of Toolbox, the HTMLElement with id
// toolbar_button_record may not exist yet.
$(document).on(
'click',
'#toolbar_button_record',
ev => this._onToolbarButtonClick(ev));
this.eventEmitter.on(UIEvents.TOGGLE_RECORDING,
() => this._onToolbarButtonClick());
// If I am a recorder then I publish my recorder custom role to notify
// everyone.
@@ -236,28 +237,6 @@ const Recording = {
}
},
/**
* Initialise the recording button.
*/
initRecordingButton() {
const selector = $('#toolbar_button_record');
selector.addClass(this.baseClass);
selector.attr('data-i18n', `[content]${this.recordingButtonTooltip}`);
APP.translation.translateElement(selector);
},
/**
* Shows or hides the 'recording' button.
* @param show {true} to show the recording button, {false} to hide it
*/
showRecordingButton(show) {
const shouldShow = show && _isRecordingButtonEnabled();
const id = 'toolbar_button_record';
UIUtil.setVisible(id, shouldShow);
},
/**
* Updates the recording state UI.
* @param recordingState gives us the current recording state
@@ -281,12 +260,12 @@ const Recording = {
* @param recordingState gives us the current recording state
*/
updateRecordingUI(recordingState) {
const oldState = this.currentState;
this.currentState = recordingState;
let labelDisplayConfiguration;
let isRecording = false;
switch (recordingState) {
case JitsiRecordingStatus.ON:
@@ -297,7 +276,7 @@ const Recording = {
showSpinner: recordingState === JitsiRecordingStatus.RETRYING
};
this._setToolbarButtonToggled(true);
isRecording = true;
break;
}
@@ -322,8 +301,6 @@ const Recording = {
: this.recordingOffKey
};
this._setToolbarButtonToggled(false);
setTimeout(() => {
APP.store.dispatch(hideRecordingLabel());
}, 5000);
@@ -337,8 +314,6 @@ const Recording = {
key: this.recordingPendingKey
};
this._setToolbarButtonToggled(false);
break;
}
@@ -348,8 +323,6 @@ const Recording = {
key: this.recordingErrorKey
};
this._setToolbarButtonToggled(false);
break;
}
@@ -362,6 +335,7 @@ const Recording = {
}
APP.store.dispatch(updateRecordingState({
isRecording,
labelDisplayConfiguration,
recordingState
}));
@@ -416,10 +390,13 @@ const Recording = {
case JitsiRecordingStatus.OFF: {
if (this.recordingType === 'jibri') {
_requestLiveStreamId()
.then(streamId => {
.then(({ broadcastId, streamId }) => {
this.eventEmitter.emit(
UIEvents.RECORDING_TOGGLED,
{ streamId });
{
broadcastId,
streamId
});
// The confirm button on the start recording dialog was
// clicked
@@ -479,16 +456,6 @@ const Recording = {
});
}
}
},
/**
* Sets the toggled state of the recording toolbar button.
*
* @param {boolean} isToggled indicates if the button should be toggled
* or not
*/
_setToolbarButtonToggled(isToggled) {
$('#toolbar_button_record').toggleClass('toggled', isToggled);
}
};

View File

@@ -18,7 +18,11 @@ import {
participantJoined,
participantLeft
} from '../../../react/features/base/participants';
import { dockToolbox, showToolbox } from '../../../react/features/toolbox';
import {
dockToolbox,
getToolboxHeight,
showToolbox
} from '../../../react/features/toolbox';
import SharedVideoThumb from './SharedVideoThumb';
@@ -282,7 +286,7 @@ export default class SharedVideoManager {
thumb.setDisplayName('YouTube');
VideoLayout.addRemoteVideoContainer(self.url, thumb);
VideoLayout.resizeThumbnails(false, true);
VideoLayout.resizeThumbnails(true);
const iframe = player.getIframe();
@@ -360,7 +364,6 @@ export default class SharedVideoManager {
player.setVolume(attributes.volume);
logger.info(`Player change of volume:${attributes.volume}`);
this.showSharedVideoMutedPopup(false);
}
if (isPlayerPaused) {
@@ -560,8 +563,6 @@ export default class SharedVideoManager {
this.smartAudioMute();
}
}
this.showSharedVideoMutedPopup(mute);
}
/**
@@ -576,7 +577,6 @@ export default class SharedVideoManager {
sendAnalytics(createEvent('audio.unmuted'));
logger.log('Shared video: audio unmuted');
this.emitter.emit(UIEvents.AUDIO_MUTED, false, false);
this.showMicMutedPopup(false);
}
}
@@ -590,38 +590,8 @@ export default class SharedVideoManager {
sendAnalytics(createEvent('audio.muted'));
logger.log('Shared video: audio muted');
this.emitter.emit(UIEvents.AUDIO_MUTED, true, false);
this.showMicMutedPopup(true);
}
}
/**
* Shows a popup under the microphone toolbar icon that notifies the user
* of automatic mute after a shared video has started.
* @param show boolean, show or hide the notification
*/
showMicMutedPopup(show) {
if (show) {
this.showSharedVideoMutedPopup(false);
}
APP.UI.showCustomToolbarPopup(
'microphone', 'micMutedPopup', show, 5000);
}
/**
* Shows a popup under the shared video toolbar icon that notifies the user
* of automatic mute of the shared video after the user has unmuted their
* mic.
* @param show boolean, show or hide the notification
*/
showSharedVideoMutedPopup(show) {
if (show) {
this.showMicMutedPopup(false);
}
APP.UI.showCustomToolbarPopup(
'sharedvideo', 'sharedVideoMutedPopup', show, 5000);
}
}
/**
@@ -695,7 +665,7 @@ class SharedVideoContainer extends LargeContainer {
let height, width;
if (interfaceConfig.VERTICAL_FILMSTRIP) {
height = containerHeight;
height = containerHeight - getToolboxHeight();
width = containerWidth - Filmstrip.getFilmstripWidth();
} else {
height = containerHeight - Filmstrip.getFilmstripHeight();

View File

@@ -1,5 +1,6 @@
/* global $ */
/* global $, APP */
import UIEvents from '../../../service/UI/UIEvents';
import { setVisiblePanel } from '../../../react/features/side-panel';
/**
* Handles open and close of the extended toolbar side panel
@@ -57,6 +58,7 @@ const SideContainerToggler = {
if (isSelectorVisible) {
this.hide();
APP.store.dispatch(setVisiblePanel(null));
} else {
if (this.isVisible()) {
$('#sideToolbarContainer').children()
@@ -74,6 +76,7 @@ const SideContainerToggler = {
}
this.showInnerContainer(elementSelector);
APP.store.dispatch(setVisiblePanel(elementId));
}
},

View File

@@ -1,7 +1,6 @@
import Chat from './chat/Chat';
import SettingsMenu from './settings/SettingsMenu';
import Profile from './profile/Profile';
import ContactListView from './contactlist/ContactListView';
import { isButtonEnabled } from '../../../react/features/toolbox';
const SidePanels = {
@@ -20,11 +19,6 @@ const SidePanels = {
if (isButtonEnabled('profile')) {
Profile.init(eventEmitter);
}
// Initialize contact list view
if (isButtonEnabled('contacts')) {
ContactListView.init();
}
}
};

View File

@@ -1,7 +1,6 @@
/* global APP, $ */
import { processReplacements, linkify } from './Replacement';
import CommandsProcessor from './Commands';
import { processReplacements } from './Replacement';
import VideoLayout from '../../videolayout/VideoLayout';
import UIUtil from '../../util/UIUtil';
@@ -9,7 +8,11 @@ import UIEvents from '../../../../service/UI/UIEvents';
import { smileys } from './smileys';
import { dockToolbox, setSubject } from '../../../../react/features/toolbox';
import { addMessage, markAllRead } from '../../../../react/features/chat';
import {
dockToolbox,
getToolboxHeight
} from '../../../../react/features/toolbox';
let unreadMessages = 0;
const sidePanelsContainerId = 'sideToolbarContainer';
@@ -163,6 +166,8 @@ function addSmileys() {
* Resizes the chat conversation.
*/
function resizeChatConversation() {
// FIXME: this function can all be done with CSS. If Chat is ever rewritten,
// do not copy over this logic.
const msgareaHeight = $('#usermsg').outerHeight();
const chatspace = $(`#${CHAT_CONTAINER_ID}`);
const width = chatspace.width();
@@ -173,7 +178,12 @@ function resizeChatConversation() {
$('#smileys').css('bottom', (msgareaHeight - 26) / 2);
$('#smileysContainer').css('bottom', msgareaHeight);
chat.width(width - 10);
chat.height(window.innerHeight - 15 - msgareaHeight);
const maybeAMagicNumberForPaddingAndMargin = 100;
const offset = maybeAMagicNumberForPaddingAndMargin
+ msgareaHeight + getToolboxHeight();
chat.height(window.innerHeight - offset);
}
/**
@@ -223,15 +233,10 @@ const Chat = {
usermsg.val('').trigger('autosize.resize');
this.focus();// eslint-disable-line no-invalid-this
const command = new CommandsProcessor(value, eventEmitter);
if (command.isCommand()) {
command.processCommand();
} else {
const message = UIUtil.escapeHtml(value);
const message = UIUtil.escapeHtml(value);
eventEmitter.emit(UIEvents.MESSAGE_CREATED, message);
}
eventEmitter.emit(UIEvents.MESSAGE_CREATED, message);
}
});
@@ -249,6 +254,7 @@ const Chat = {
}
unreadMessages = 0;
APP.store.dispatch(markAllRead());
updateVisualNotification();
// Undock the toolbar when the chat is shown and if we're in a
@@ -274,9 +280,10 @@ const Chat = {
*/
// eslint-disable-next-line max-params
updateChatConversation(id, displayName, message, stamp) {
const isFromLocalParticipant = APP.conference.isLocalId(id);
let divClassName = '';
if (APP.conference.isLocalId(id)) {
if (isFromLocalParticipant) {
divClassName = 'localuser';
} else {
divClassName = 'remoteuser';
@@ -294,6 +301,7 @@ const Chat = {
.replace(/>/g, '&gt;')
.replace(/\n/g, '<br/>');
const escDisplayName = UIUtil.escapeHtml(displayName);
const timestamp = getCurrentTime(stamp);
// eslint-disable-next-line no-param-reassign
message = processReplacements(escMessage);
@@ -302,13 +310,18 @@ const Chat = {
= `${'<div class="chatmessage">'
+ '<img src="images/chatArrow.svg" class="chatArrow">'
+ '<div class="username '}${divClassName}">${escDisplayName
}</div><div class="timestamp">${getCurrentTime(stamp)
}</div><div class="timestamp">${timestamp
}</div><div class="usermessage">${message}</div>`
+ '</div>';
$('#chatconversation').append(messageContainer);
$('#chatconversation').animate(
{ scrollTop: $('#chatconversation')[0].scrollHeight }, 1000);
const markAsRead = Chat.isVisible() || isFromLocalParticipant;
APP.store.dispatch(addMessage(
escDisplayName, message, timestamp, markAsRead));
},
/**
@@ -331,21 +344,6 @@ const Chat = {
{ scrollTop: $('#chatconversation')[0].scrollHeight }, 1000);
},
/**
* Sets the subject to the UI
* @param subject the subject
*/
setSubject(subject) {
if (subject) {
// eslint-disable-next-line no-param-reassign
subject = subject.trim();
}
const html = linkify(UIUtil.escapeHtml(subject));
APP.store.dispatch(setSubject(html));
},
/**
* Sets the chat conversation mode.
* Conversation mode is the normal chat mode, non conversation mode is

View File

@@ -1,94 +0,0 @@
import UIUtil from '../../util/UIUtil';
import UIEvents from '../../../../service/UI/UIEvents';
/**
* List with supported commands. The keys are the names of the commands and
* the value is the function that processes the message.
* @type {{String: function}}
*/
const commands = {
'topic': processTopic
};
/**
* Extracts the command from the message.
* @param message the received message
* @returns {string} the command
*/
function getCommand(message) {
if (message) {
for (const command in commands) {
if (message.indexOf(`/${command}`) === 0) {
return command;
}
}
}
return '';
}
/**
* Processes the data for topic command.
* @param commandArguments the arguments of the topic command.
*/
function processTopic(commandArguments, emitter) {
const topic = UIUtil.escapeHtml(commandArguments);
emitter.emit(UIEvents.SUBJECT_CHANGED, topic);
}
/**
* Constructs a new CommandProccessor instance from a message that
* handles commands received via chat messages.
* @param message the message
* @constructor
*/
function CommandsProcessor(message, emitter) {
const command = getCommand(message);
this.emitter = emitter;
/**
* Returns the name of the command.
* @returns {String} the command
*/
this.getCommand = function() {
return command;
};
const messageArgument = message.substr(command.length + 2);
/**
* Returns the arguments of the command.
* @returns {string}
*/
this.getArgument = function() {
return messageArgument;
};
}
/**
* Checks whether this instance is valid command or not.
* @returns {boolean}
*/
CommandsProcessor.prototype.isCommand = function() {
if (this.getCommand()) {
return true;
}
return false;
};
/**
* Processes the command.
*/
CommandsProcessor.prototype.processCommand = function() {
if (!this.isCommand()) {
return;
}
commands[this.getCommand()](this.getArgument(), this.emitter);
};
export default CommandsProcessor;

View File

@@ -1,56 +0,0 @@
/* global $, APP */
/* 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 { ContactListPanel } from '../../../../react/features/contact-list';
/* eslint-enable no-unused-vars */
import UIUtil from '../../util/UIUtil';
/**
* Contact list.
*
* FIXME: One day this view should no longer be called "contact list" because
* the term "contact" is not used elsewhere. Normally people in the conference
* are internally refered to as "participants" or externally as "members".
*/
const ContactListView = {
/**
* Creates and appends the contact list to the side panel.
*
* @returns {void}
*/
init() {
const contactListPanelContainer = document.createElement('div');
contactListPanelContainer.id = 'contacts_container';
contactListPanelContainer.className = 'sideToolbarContainer__inner';
$('#sideToolbarContainer').append(contactListPanelContainer);
ReactDOM.render(
<Provider store = { APP.store }>
<I18nextProvider i18n = { i18next }>
<ContactListPanel />
</I18nextProvider>
</Provider>,
contactListPanelContainer
);
},
/**
* Indicates if the contact list is currently visible.
*
* @return {boolean) true if the contact list is currently visible.
*/
isVisible() {
return UIUtil.isVisible(document.getElementById('contactlist'));
}
};
export default ContactListView;

View File

@@ -227,43 +227,10 @@ const UIUtil = {
* mode, {false} otherwise
*/
isFullScreen() {
return document.fullscreenElement
return Boolean(document.fullscreenElement
|| document.mozFullScreenElement
|| document.webkitFullscreenElement
|| document.msFullscreenElement;
},
/**
* Exits full screen mode.
* @see https://developer.mozilla.org/en-US/docs/Web/API/Fullscreen_API
*/
exitFullScreen() {
if (document.exitFullscreen) {
document.exitFullscreen();
} else if (document.msExitFullscreen) {
document.msExitFullscreen();
} else if (document.mozCancelFullScreen) {
document.mozCancelFullScreen();
} else if (document.webkitExitFullscreen) {
document.webkitExitFullscreen();
}
},
/**
* Enter full screen mode.
* @see https://developer.mozilla.org/en-US/docs/Web/API/Fullscreen_API
*/
enterFullScreen() {
if (document.documentElement.requestFullscreen) {
document.documentElement.requestFullscreen();
} else if (document.documentElement.msRequestFullscreen) {
document.documentElement.msRequestFullscreen();
} else if (document.documentElement.mozRequestFullScreen) {
document.documentElement.mozRequestFullScreen();
} else if (document.documentElement.webkitRequestFullscreen) {
document.documentElement
.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT);
}
|| document.msFullscreenElement);
},
/**

View File

@@ -438,87 +438,56 @@ const Filmstrip = {
* Resizes thumbnails
* @param local
* @param remote
* @param animate
* @param forceUpdate
* @returns {Promise}
*/
// eslint-disable-next-line max-params
resizeThumbnails(local, remote, animate = false, forceUpdate = false) {
return new Promise(resolve => {
const thumbs = this.getThumbs(!forceUpdate);
const promises = [];
resizeThumbnails(local, remote, forceUpdate = false) {
const thumbs = this.getThumbs(!forceUpdate);
if (thumbs.localThumb) {
// eslint-disable-next-line no-shadow
promises.push(new Promise(resolve => {
thumbs.localThumb.animate({
height: local.thumbHeight,
'min-height': local.thumbHeight,
'min-width': local.thumbWidth,
width: local.thumbWidth
}, this._getAnimateOptions(animate, resolve));
}));
}
if (thumbs.remoteThumbs) {
// eslint-disable-next-line no-shadow
promises.push(new Promise(resolve => {
thumbs.remoteThumbs.animate({
height: remote.thumbHeight,
'min-height': remote.thumbHeight,
'min-width': remote.thumbWidth,
width: remote.thumbWidth
}, this._getAnimateOptions(animate, resolve));
}));
}
if (thumbs.localThumb) {
// eslint-disable-next-line no-shadow
promises.push(new Promise(resolve => {
// Let CSS take care of height in vertical filmstrip mode.
if (interfaceConfig.VERTICAL_FILMSTRIP) {
$('#filmstripLocalVideo').animate({
// adds 4 px because of small video 2px border
width: local.thumbWidth + 4
}, this._getAnimateOptions(animate, resolve));
} else {
this.filmstrip.animate({
// adds 4 px because of small video 2px border
height: remote.thumbHeight + 4
}, this._getAnimateOptions(animate, resolve));
}
}));
thumbs.localThumb.css({
display: 'inline-block',
height: `${local.thumbHeight}px`,
'min-height': `${local.thumbHeight}px`,
'min-width': `${local.thumbWidth}px`,
width: `${local.thumbWidth}px`
});
}
promises.push(new Promise(() => {
const { localThumb } = this.getThumbs();
const height = localThumb ? localThumb.height() : 0;
const fontSize = UIUtil.getIndicatorFontSize(height);
if (thumbs.remoteThumbs) {
thumbs.remoteThumbs.css({
display: 'inline-block',
height: `${remote.thumbHeight}px`,
'min-height': `${remote.thumbHeight}px`,
'min-width': `${remote.thumbWidth}px`,
width: `${remote.thumbWidth}px`
});
}
this.filmstrip.find('.indicator').animate({
fontSize
}, this._getAnimateOptions(animate, resolve));
}));
// Let CSS take care of height in vertical filmstrip mode.
if (interfaceConfig.VERTICAL_FILMSTRIP) {
$('#filmstripLocalVideo').css({
// adds 4 px because of small video 2px border
width: `${local.thumbWidth + 4}px`
});
} else {
this.filmstrip.css({
// adds 4 px because of small video 2px border
height: `${remote.thumbHeight + 4}px`
});
}
if (!animate) {
resolve();
}
const { localThumb } = this.getThumbs();
const height = localThumb ? localThumb.height() : 0;
const fontSize = UIUtil.getIndicatorFontSize(height);
Promise.all(promises).then(resolve);
this.filmstrip.find('.indicator').css({
'font-size': `${fontSize}px`
});
},
/**
* Helper method. Returns options for jQuery animation
* @param animate {Boolean} - animation flag
* @param cb {Function} - complete callback
* @returns {Object} - animation options object
* @private
*/
_getAnimateOptions(animate, cb = $.noop) {
return {
queue: false,
duration: animate ? 500 : 0,
complete: cb
};
},
/**
* Returns thumbnails of the filmstrip
* @param onlyVisible

View File

@@ -95,7 +95,7 @@ RemoteVideo.prototype.addRemoteVideoContainer = function() {
this.updateRemoteVideoMenu();
this.VideoLayout.resizeThumbnails(false, true);
this.VideoLayout.resizeThumbnails(true);
this.addAudioLevelIndicator();

View File

@@ -66,9 +66,6 @@ const VideoLayout = {
init(emitter) {
eventEmitter = emitter;
// Unregister listeners in case of reinitialization
this.unregisterListeners();
localVideoThumbnail = new LocalVideo(VideoLayout, emitter);
// sets default video type of local video
@@ -77,7 +74,7 @@ const VideoLayout = {
// if we do not resize the thumbs here, if there is no video device
// the local video thumb maybe one pixel
this.resizeThumbnails(false, true);
this.resizeThumbnails(true);
this.handleVideoThumbClicked = this.handleVideoThumbClicked.bind(this);
@@ -104,20 +101,6 @@ const VideoLayout = {
registerListeners() {
eventEmitter.addListener(UIEvents.LOCAL_FLIPX_CHANGED,
onLocalFlipXChanged);
eventEmitter.addListener(UIEvents.CONTACT_CLICKED,
this.handleVideoThumbClicked);
},
/**
* Unregistering listeners for UI events in Video layout component.
*
* @returns {void}
*/
unregisterListeners() {
if (this._onContactClicked) {
eventEmitter.removeListener(UIEvents.CONTACT_CLICKED,
this.handleVideoThumbClicked);
}
},
initLargeVideo() {
@@ -485,7 +468,7 @@ const VideoLayout = {
remoteVideo.setVideoType(VIDEO_CONTAINER_TYPE);
}
VideoLayout.resizeThumbnails(false, true);
VideoLayout.resizeThumbnails(true);
// Initialize the view
remoteVideo.updateView();
@@ -497,7 +480,7 @@ const VideoLayout = {
logger.info(`${resourceJid} video is now active`, videoElement);
VideoLayout.resizeThumbnails(
false, false, () => {
false, () => {
if (videoElement) {
$(videoElement).show();
}
@@ -592,19 +575,16 @@ const VideoLayout = {
* Resizes thumbnails.
*/
resizeThumbnails(
animate = false,
forceUpdate = false,
onComplete = null) {
const { localVideo, remoteVideo }
= Filmstrip.calculateThumbnailSize();
Filmstrip.resizeThumbnails(localVideo, remoteVideo,
animate, forceUpdate)
.then(() => {
if (onComplete && typeof onComplete === 'function') {
onComplete();
}
});
Filmstrip.resizeThumbnails(localVideo, remoteVideo, forceUpdate);
if (onComplete && typeof onComplete === 'function') {
onComplete();
}
return { localVideo,
remoteVideo };
@@ -896,7 +876,7 @@ const VideoLayout = {
}
// Resize the thumbnails first.
this.resizeThumbnails(false, forceUpdate);
this.resizeThumbnails(forceUpdate);
// Resize the video area element.
$('#videospace').animate({
@@ -1112,7 +1092,7 @@ const VideoLayout = {
* Currently used by tests (torture).
*/
getLargeVideoID() {
return largeVideo.id;
return largeVideo && largeVideo.id;
},
/**

View File

@@ -15,9 +15,9 @@ const logger = require('jitsi-meet-logger').getLogger(__filename);
/**
* Map of shortcuts. When a shortcut is registered it enters the mapping.
* @type {{}}
* @type {Map}
*/
const _shortcuts = {};
const _shortcuts = new Map();
/**
* Map of registered keyboard keys and translation keys describing the
@@ -49,8 +49,8 @@ const KeyboardShortcut = {
if (!($(':focus').is('input[type=text]')
|| $(':focus').is('input[type=password]')
|| $(':focus').is('textarea'))) {
if (_shortcuts.hasOwnProperty(key)) {
_shortcuts[key].function(e);
if (_shortcuts.has(key)) {
_shortcuts.get(key).function(e);
} else if (!isNaN(num) && num >= 0 && num <= 9) {
APP.UI.clickOnVideo(num);
}
@@ -90,6 +90,17 @@ const KeyboardShortcut = {
enabled = value;
},
/**
* Opens the {@KeyboardShortcutsDialog} dialog.
*
* @returns {void}
*/
openDialog() {
APP.store.dispatch(toggleDialog(KeyboardShortcutsDialog, {
shortcutDescriptions: _shortcutsHelp
}));
},
/**
* Registers a new shortcut.
*
@@ -106,11 +117,11 @@ const KeyboardShortcut = {
shortcutAttr,
exec,
helpDescription) {
_shortcuts[shortcutChar] = {
_shortcuts.set(shortcutChar, {
character: shortcutChar,
shortcutAttr,
function: exec
};
function: exec,
shortcutAttr
});
if (helpDescription) {
this._addShortcutToHelp(shortcutChar, helpDescription);
@@ -124,7 +135,7 @@ const KeyboardShortcut = {
* no longer be usable
*/
unregisterShortcut(shortcutChar) {
_shortcuts.remove(shortcutChar);
_shortcuts.delete(shortcutChar);
_shortcutsHelp.delete(shortcutChar);
},
@@ -133,7 +144,12 @@ const KeyboardShortcut = {
* @returns {string} e.key or something close if not supported
*/
_getKeyboardKey(e) {
if (typeof e.key === 'string') {
// If e.key is a string, then it is assumed it already plainly states
// the key pressed. This may not be true in all cases, such as with Edge
// and "?", when the browser cannot properly map a key press event to a
// keyboard key. To be safe, when a key is "Unidentified" it must be
// further analyzed by jitsi to a key using e.which.
if (typeof e.key === 'string' && e.key !== 'Unidentified') {
return e.key;
}
if (e.type === 'keypress'
@@ -177,9 +193,7 @@ const KeyboardShortcut = {
_initGlobalShortcuts() {
this.registerShortcut('?', null, () => {
sendAnalytics(createShortcutEvent('help'));
APP.store.dispatch(toggleDialog(KeyboardShortcutsDialog, {
shortcutDescriptions: _shortcutsHelp
}));
this.openDialog();
}, 'keyboardShortcuts.toggleShortcuts');
// register SPACE shortcut in two steps to insure visibility of help

View File

@@ -22,7 +22,6 @@ const LEGACY_INCOMING_METHODS = [
'email',
'toggle-audio',
'toggle-chat',
'toggle-contact-list',
'toggle-film-strip',
'toggle-share-screen',
'toggle-video',

View File

@@ -0,0 +1,50 @@
/**
* Implements in memory logs storage, used for testing/debugging.
*/
export default class JitsiMeetInMemoryLogStorage {
/**
* Creates new <tt>JitsiMeetInMemoryLogStorage</tt>
*/
constructor() {
/**
* Array of the log entries to keep.
* @type {array}
*/
this.logs = [];
}
/**
* @returns {boolean} <tt>true</tt> when this storage is ready or
* <tt>false</tt> otherwise.
*/
isReady() {
return true;
}
/**
* Called by the <tt>LogCollector</tt> to store a series of log lines into
* batch.
* @param {string|object[]} logEntries an array containing strings
* representing log lines or aggregated lines objects.
*/
storeLogs(logEntries) {
for (let i = 0, len = logEntries.length; i < len; i++) {
const logEntry = logEntries[i];
if (typeof logEntry === 'object') {
this.logs.push(logEntry.text);
} else {
// Regular message
this.logs.push(logEntry);
}
}
}
/**
* @returns {array} the collected log entries.
*/
getLogs() {
return this.logs;
}
}

494
package-lock.json generated
View File

@@ -91,7 +91,7 @@
"@atlaskit/layer": "2.7.2",
"@atlaskit/spinner": "4.0.0",
"@atlaskit/theme": "2.4.0",
"@atlaskit/tooltip": "6.0.0",
"@atlaskit/tooltip": "6.2.2",
"babel-runtime": "6.26.0",
"classnames": "2.2.5",
"keycode": "2.1.9",
@@ -109,6 +109,19 @@
"styled-components": "1.4.6"
}
},
"@atlaskit/tooltip": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/@atlaskit/tooltip/-/tooltip-6.2.2.tgz",
"integrity": "sha1-zHE3McH4BNnYAXYRGzk6c3DXUdo=",
"requires": {
"@atlaskit/layer": "2.7.2",
"@atlaskit/theme": "2.4.0",
"@atlaskit/util-shared-styles": "2.10.6",
"babel-runtime": "6.26.0",
"prop-types": "15.6.0",
"styled-components": "1.4.6"
}
},
"styled-components": {
"version": "1.4.6",
"resolved": "https://registry.npmjs.org/styled-components/-/styled-components-1.4.6.tgz",
@@ -409,49 +422,91 @@
}
},
"@atlaskit/inline-dialog": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/@atlaskit/inline-dialog/-/inline-dialog-5.0.2.tgz",
"integrity": "sha1-xwPi9seo0M+nrcPXgJK0bE7ShkQ=",
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/@atlaskit/inline-dialog/-/inline-dialog-5.3.0.tgz",
"integrity": "sha512-4bEeC5rZwtb4YO9BxW1UCJYCp/dyCVXqcygRW1BDnYVbveAI8wdym6qEi4BRvIwXCT4qgNhsVsqcxSrn0X6CKQ==",
"requires": {
"@atlaskit/layer": "2.7.2",
"@atlaskit/layer": "2.9.1",
"@atlaskit/theme": "2.4.0",
"babel-runtime": "6.26.0",
"prop-types": "15.6.0",
"styled-components": "1.4.6"
}
},
"@atlaskit/inline-message": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@atlaskit/inline-message/-/inline-message-3.0.1.tgz",
"integrity": "sha1-VIF4Yg+CHx/nFBI+E4dQh9Upn78=",
"requires": {
"@atlaskit/button": "3.6.0",
"@atlaskit/icon": "7.1.0",
"@atlaskit/inline-dialog": "5.0.2",
"@atlaskit/theme": "2.4.0",
"babel-runtime": "6.26.0",
"prop-types": "15.6.0",
"styled-components": "1.4.6"
},
"dependencies": {
"@atlaskit/button": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/@atlaskit/button/-/button-3.6.0.tgz",
"integrity": "sha1-tx5uySqBkHWAfqqbItyV59NSgqE=",
"@atlaskit/layer": {
"version": "2.9.1",
"resolved": "https://registry.npmjs.org/@atlaskit/layer/-/layer-2.9.1.tgz",
"integrity": "sha512-nyIVGeS2OhuGR5gIMTYUfRmCG8z/9KMgUzTpbpsB70sH6+d4KSFhfkz+KhKNIa8gvKI6zBc+3UBYSlUW1t1qmQ==",
"requires": {
"@atlaskit/util-shared-styles": "2.10.6",
"styled-components": "1.4.6"
}
}
}
},
"@atlaskit/inline-message": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@atlaskit/inline-message/-/inline-message-4.0.0.tgz",
"integrity": "sha512-JhZf3Oux7kp+7RMFIGuaVblGiwWPd8EfCQ2kJXSvxYFTRo798gPFBKz4LU/NKQU3Fl2Rht6o5qmvjbdbzoxcZg==",
"requires": {
"@atlaskit/button": "7.0.0",
"@atlaskit/icon": "11.0.0",
"@atlaskit/inline-dialog": "6.0.0",
"@atlaskit/theme": "3.0.0",
"styled-components": "1.4.6"
},
"dependencies": {
"@atlaskit/analytics-next": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@atlaskit/analytics-next/-/analytics-next-2.0.0.tgz",
"integrity": "sha512-HHRifSrETd/hR7vj3Vqscz4374G1hhEnSehFkPvP+2BIL64TwsqlDLjwL/y1j+/5tFpSYRjfIAVBM/RcCuuzzg==",
"requires": {
"prop-types": "15.6.0"
}
},
"@atlaskit/button": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/@atlaskit/button/-/button-7.0.0.tgz",
"integrity": "sha512-FXozSxBcmigSt3+ff+ca8kfEox+mP6dJsXG4i/fkYfsmZr8WXYnyPWndWvOJYs7jvqqP9FMIGSJJQeOJHI60Ow==",
"requires": {
"@atlaskit/analytics-next": "2.0.0",
"@atlaskit/theme": "3.0.0",
"babel-runtime": "6.26.0",
"classnames": "2.2.5",
"prop-types": "15.6.0",
"styled-components": "1.4.6"
}
},
"@atlaskit/icon": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/@atlaskit/icon/-/icon-7.1.0.tgz",
"integrity": "sha1-czQVCEhzUmPeShO8mPTUTU8r6N8=",
"version": "11.0.0",
"resolved": "https://registry.npmjs.org/@atlaskit/icon/-/icon-11.0.0.tgz",
"integrity": "sha512-KxGxDwbuWeGZ5ivP1JFlPOhHipQQzgm0lAUGzziaYi6yOaNmfwmNjpj2xBpOhlkkymvhnFkzV5JSwVUH+0rW1w==",
"requires": {
"@atlaskit/theme": "3.0.0",
"babel-runtime": "6.26.0",
"styled-components": "1.4.6",
"uuid": "3.1.0"
}
},
"@atlaskit/inline-dialog": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/@atlaskit/inline-dialog/-/inline-dialog-6.0.0.tgz",
"integrity": "sha512-ITGozbuJg6UFzZHQjrWOsx17jKMFR4TE6YdTYA2UTKmKJalHvH414APuNyZdIz8XCFFnQo8NuGdkAjiBYCKG0w==",
"requires": {
"@atlaskit/layer": "3.1.0",
"@atlaskit/theme": "3.0.0",
"styled-components": "1.4.6"
}
},
"@atlaskit/layer": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@atlaskit/layer/-/layer-3.1.0.tgz",
"integrity": "sha512-QdOiu/c0u+YZKuC5z1hYZj/TW0axO5pL2kWBU2MLyMnThfHA+8UCMvIKtpI5/DoMevR5bEuLTX6ivos15l2ulA==",
"requires": {
"react-scrolllock": "2.0.2",
"styled-components": "1.4.6"
}
},
"@atlaskit/theme": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@atlaskit/theme/-/theme-3.0.0.tgz",
"integrity": "sha512-j9n0x+WjpwLelPR6Mf64meuPSWsHBK7hEU6q3dLK5YerqXaocu9Z5Dcnv6E8F+zfzGvPn/9ISgO2L3ts5Gv/3g==",
"requires": {
"prop-types": "15.6.0",
"styled-components": "1.4.6"
}
@@ -838,16 +893,50 @@
}
},
"@atlaskit/tooltip": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/@atlaskit/tooltip/-/tooltip-6.0.0.tgz",
"integrity": "sha1-rn3xMCmvO1iZqcuKNPPIT8K7mpc=",
"version": "9.1.1",
"resolved": "https://registry.npmjs.org/@atlaskit/tooltip/-/tooltip-9.1.1.tgz",
"integrity": "sha512-LGMg+OP9qRIMzr8DJtMUBAkHbCN9Vwvh30QWGu/pXktBaxSM5JbxGnUughnoE1JFYyeycJGmyZ0ibtQDnmiB7w==",
"requires": {
"@atlaskit/layer": "2.7.2",
"@atlaskit/theme": "2.4.0",
"@atlaskit/util-shared-styles": "2.10.6",
"babel-runtime": "6.26.0",
"prop-types": "15.6.0",
"@atlaskit/layer-manager": "3.0.1",
"@atlaskit/theme": "3.0.0",
"react-deprecate": "0.1.0",
"react-transition-group": "2.3.0",
"styled-components": "1.4.6"
},
"dependencies": {
"@atlaskit/layer-manager": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@atlaskit/layer-manager/-/layer-manager-3.0.1.tgz",
"integrity": "sha512-UPACU1LI+mseZ+GM5HDAHVAONemCQlVMVDlGAW77jYyG4rrCwQUzjZNS8OULvCfHZyULDrvT/w9FqG6eKPTo+A==",
"requires": {
"focusin": "2.0.0",
"prop-types": "15.6.0",
"react-transition-group": "2.3.0",
"styled-components": "1.4.6",
"tabbable": "1.1.2"
}
},
"@atlaskit/theme": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@atlaskit/theme/-/theme-3.0.0.tgz",
"integrity": "sha512-j9n0x+WjpwLelPR6Mf64meuPSWsHBK7hEU6q3dLK5YerqXaocu9Z5Dcnv6E8F+zfzGvPn/9ISgO2L3ts5Gv/3g==",
"requires": {
"prop-types": "15.6.0",
"styled-components": "1.4.6"
}
},
"react-transition-group": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-2.3.0.tgz",
"integrity": "sha512-OU3/swEL8y233u5ajTn3FIcQQ/b3XWjLXB6e2LnM1OK5JATtsyfJvPTZ8c/dawHNqjUltcdHRSpgMtPe7v07pw==",
"requires": {
"chain-function": "1.0.0",
"dom-helpers": "3.3.1",
"loose-envify": "1.3.1",
"prop-types": "15.6.0",
"warning": "3.0.0"
}
}
}
},
"@atlaskit/util-common": {
@@ -4695,6 +4784,11 @@
"strip-eof": "1.0.0"
}
},
"exenv": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/exenv/-/exenv-1.2.2.tgz",
"integrity": "sha1-KueOhdmJQVhnCwPUe+wfA72Ru50="
},
"exit": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz",
@@ -6236,6 +6330,21 @@
"globule": "1.2.0"
}
},
"generate-function": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.0.0.tgz",
"integrity": "sha1-aFj+fAlpt9TpCTM3ZHrHn2DfvnQ=",
"dev": true
},
"generate-object-property": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz",
"integrity": "sha1-nA4cQDCM6AT0eDYYuTf6iPmdUNA=",
"dev": true,
"requires": {
"is-property": "1.0.2"
}
},
"get-caller-file": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.2.tgz",
@@ -6785,11 +6894,6 @@
"resolved": "https://registry.npmjs.org/image-size/-/image-size-0.6.2.tgz",
"integrity": "sha512-pH3vDzpczdsKHdZ9xxR3O46unSjisgVx0IImay7Zz2EdhRVbCkj+nthx9OuuWEhakx9FAO+fNVGrF0rZ2oMOvw=="
},
"immutable": {
"version": "3.8.2",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-3.8.2.tgz",
"integrity": "sha1-wkOZUUVbs5kT2vKBN28VMOEErfM="
},
"import-local": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/import-local/-/import-local-0.1.1.tgz",
@@ -7066,6 +7170,25 @@
"is-extglob": "1.0.0"
}
},
"is-my-ip-valid": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-my-ip-valid/-/is-my-ip-valid-1.0.0.tgz",
"integrity": "sha512-gmh/eWXROncUzRnIa1Ubrt5b8ep/MGSnfAUI3aRp+sqTCs1tv1Isl8d8F6JmkN3dXKc3ehZMrtiPN9eL03NuaQ==",
"dev": true
},
"is-my-json-valid": {
"version": "2.17.2",
"resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.17.2.tgz",
"integrity": "sha512-IBhBslgngMQN8DDSppmgDv7RNrlFotuuDsKcrCP3+HbFaVivIBU7u9oiiErw8sH4ynx3+gOGQ3q2otkgiSi6kg==",
"dev": true,
"requires": {
"generate-function": "2.0.0",
"generate-object-property": "1.2.0",
"is-my-ip-valid": "1.0.0",
"jsonpointer": "4.0.1",
"xtend": "4.0.1"
}
},
"is-number": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz",
@@ -7127,6 +7250,12 @@
"resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz",
"integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o="
},
"is-property": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz",
"integrity": "sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ=",
"dev": true
},
"is-regex": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz",
@@ -7386,6 +7515,12 @@
"resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz",
"integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM="
},
"jsonpointer": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.0.1.tgz",
"integrity": "sha1-T9kss04OnbPInIYi7PUfm5eMbLk=",
"dev": true
},
"jsprim": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz",
@@ -7473,7 +7608,7 @@
}
},
"lib-jitsi-meet": {
"version": "github:jitsi/lib-jitsi-meet#0503ec4d3f175b154b1c6fd7037520ce0768fa58",
"version": "github:jitsi/lib-jitsi-meet#307777207ec219df344b8fed2fd2071d2238f9f8",
"requires": {
"async": "0.9.0",
"current-executing-script": "0.1.3",
@@ -7639,6 +7774,12 @@
"integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=",
"dev": true
},
"lodash.mergewith": {
"version": "4.6.1",
"resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.1.tgz",
"integrity": "sha512-eWw5r+PYICtEBgrBE5hhlT6aAa75f411bgDz/ZL2KZqYV03USvucsxcHUIlGTDTECs1eunpI7HOV7U+WLDvNdQ==",
"dev": true
},
"lodash.pad": {
"version": "4.5.1",
"resolved": "https://registry.npmjs.org/lodash.pad/-/lodash.pad-4.5.1.tgz",
@@ -8315,7 +8456,8 @@
"nan": {
"version": "2.9.2",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.9.2.tgz",
"integrity": "sha512-ltW65co7f3PQWBDbqVvaU1WtFJUsNW7sWWm4HINhbMQIyVyzIeyZ8toX5TC5eeooE6piZoaEh4cZkueSKG3KYw=="
"integrity": "sha512-ltW65co7f3PQWBDbqVvaU1WtFJUsNW7sWWm4HINhbMQIyVyzIeyZ8toX5TC5eeooE6piZoaEh4cZkueSKG3KYw==",
"optional": true
},
"natural-compare": {
"version": "1.4.0",
@@ -8488,9 +8630,9 @@
}
},
"node-sass": {
"version": "3.13.1",
"resolved": "https://registry.npmjs.org/node-sass/-/node-sass-3.13.1.tgz",
"integrity": "sha1-ckD7v/I5YwS0IjUn7TAgWJwAT8I=",
"version": "4.8.3",
"resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.8.3.tgz",
"integrity": "sha512-tfFWhUsCk/Y19zarDcPo5xpj+IW3qCfOjVdHtYeG6S1CKbQOh1zqylnQK6cV3z9k80yxAnFX9Y+a9+XysDhhfg==",
"dev": true,
"requires": {
"async-foreach": "0.1.3",
@@ -8502,15 +8644,45 @@
"in-publish": "2.0.0",
"lodash.assign": "4.2.0",
"lodash.clonedeep": "4.5.0",
"lodash.mergewith": "4.6.1",
"meow": "3.7.0",
"mkdirp": "0.5.1",
"nan": "2.9.2",
"nan": "2.10.0",
"node-gyp": "3.6.2",
"npmlog": "4.1.2",
"request": "2.83.0",
"sass-graph": "2.2.4"
"request": "2.79.0",
"sass-graph": "2.2.4",
"stdout-stream": "1.4.0",
"true-case-path": "1.0.2"
},
"dependencies": {
"assert-plus": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz",
"integrity": "sha1-104bh+ev/A24qttwIfP+SBAasjQ=",
"dev": true
},
"aws-sign2": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz",
"integrity": "sha1-FDQt0428yU0OW4fXY81jYSwOeU8=",
"dev": true
},
"boom": {
"version": "2.10.1",
"resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz",
"integrity": "sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8=",
"dev": true,
"requires": {
"hoek": "2.16.3"
}
},
"caseless": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/caseless/-/caseless-0.11.0.tgz",
"integrity": "sha1-cVuW6phBWTzDMGeSP17GDr2k99c=",
"dev": true
},
"cross-spawn": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-3.0.1.tgz",
@@ -8521,6 +8693,26 @@
"which": "1.3.0"
}
},
"cryptiles": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz",
"integrity": "sha1-O9/s3GCBR8HGcgL6KR59ylnqo7g=",
"dev": true,
"requires": {
"boom": "2.10.1"
}
},
"form-data": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-2.1.4.tgz",
"integrity": "sha1-M8GDrPGTJ27KqYFDpp6Uv+4XUNE=",
"dev": true,
"requires": {
"asynckit": "0.4.0",
"combined-stream": "1.0.6",
"mime-types": "2.1.18"
}
},
"gauge": {
"version": "2.7.4",
"resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz",
@@ -8537,6 +8729,47 @@
"wide-align": "1.1.2"
}
},
"har-validator": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/har-validator/-/har-validator-2.0.6.tgz",
"integrity": "sha1-zcvAgYgmWtEZtqWnyKtw7s+10n0=",
"dev": true,
"requires": {
"chalk": "1.1.3",
"commander": "2.14.1",
"is-my-json-valid": "2.17.2",
"pinkie-promise": "2.0.1"
}
},
"hawk": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz",
"integrity": "sha1-B4REvXwWQLD+VA0sm3PVlnjo4cQ=",
"dev": true,
"requires": {
"boom": "2.10.1",
"cryptiles": "2.0.5",
"hoek": "2.16.3",
"sntp": "1.0.9"
}
},
"hoek": {
"version": "2.16.3",
"resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz",
"integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=",
"dev": true
},
"http-signature": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz",
"integrity": "sha1-33LiZwZs0Kxn+3at+OE0qPvPkb8=",
"dev": true,
"requires": {
"assert-plus": "0.2.0",
"jsprim": "1.4.1",
"sshpk": "1.13.1"
}
},
"is-fullwidth-code-point": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
@@ -8546,6 +8779,12 @@
"number-is-nan": "1.0.1"
}
},
"nan": {
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.10.0.tgz",
"integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA==",
"dev": true
},
"npmlog": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz",
@@ -8558,6 +8797,49 @@
"set-blocking": "2.0.0"
}
},
"qs": {
"version": "6.3.2",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.3.2.tgz",
"integrity": "sha1-51vV9uJoEioqDgvaYwslUMFmUCw=",
"dev": true
},
"request": {
"version": "2.79.0",
"resolved": "https://registry.npmjs.org/request/-/request-2.79.0.tgz",
"integrity": "sha1-Tf5b9r6LjNw3/Pk+BLZVd3InEN4=",
"dev": true,
"requires": {
"aws-sign2": "0.6.0",
"aws4": "1.6.0",
"caseless": "0.11.0",
"combined-stream": "1.0.6",
"extend": "3.0.1",
"forever-agent": "0.6.1",
"form-data": "2.1.4",
"har-validator": "2.0.6",
"hawk": "3.1.3",
"http-signature": "1.1.1",
"is-typedarray": "1.0.0",
"isstream": "0.1.2",
"json-stringify-safe": "5.0.1",
"mime-types": "2.1.18",
"oauth-sign": "0.8.2",
"qs": "6.3.2",
"stringstream": "0.0.5",
"tough-cookie": "2.3.3",
"tunnel-agent": "0.4.3",
"uuid": "3.1.0"
}
},
"sntp": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz",
"integrity": "sha1-ZUEYTMkK7qbG57NeJlkIJEPGYZg=",
"dev": true,
"requires": {
"hoek": "2.16.3"
}
},
"string-width": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
@@ -8568,6 +8850,12 @@
"is-fullwidth-code-point": "1.0.0",
"strip-ansi": "3.0.1"
}
},
"tunnel-agent": {
"version": "0.4.3",
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.4.3.tgz",
"integrity": "sha1-Y3PbdpCf5XDgjXNYM2Xtgop07us=",
"dev": true
}
}
},
@@ -8644,14 +8932,6 @@
"gauge": "1.2.7"
}
},
"nuclear-js": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/nuclear-js/-/nuclear-js-1.4.0.tgz",
"integrity": "sha1-bJwAGwZz8K6dj4sYjE2gTtaTp74=",
"requires": {
"immutable": "3.8.2"
}
},
"num2fraction": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/num2fraction/-/num2fraction-1.2.2.tgz",
@@ -10010,6 +10290,11 @@
"resolved": "https://registry.npmjs.org/react-deep-force-update/-/react-deep-force-update-1.1.1.tgz",
"integrity": "sha1-vNMUeAJ7ZLMznxCJIatSC0MT3Cw="
},
"react-deprecate": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/react-deprecate/-/react-deprecate-0.1.0.tgz",
"integrity": "sha512-9ooyaovhANHgfuOxXRgrEiEfWjEhvygeSxrRTGxNlXErnXnyHBGjxCxrKYsT/Gsc62lS9rFOBeK0c2wwdyUnvQ=="
},
"react-devtools-core": {
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/react-devtools-core/-/react-devtools-core-2.5.2.tgz",
@@ -10156,9 +10441,7 @@
"integrity": "sha512-vLNJIedXQZN4p3ChFsAgVHacnJqQMnLl+wBsnZuliRkmsjEHo8kQOA9fnLih/OoiDi1O3eHQvXC5L8f+RYiKgw=="
},
"react-native-calendar-events": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/react-native-calendar-events/-/react-native-calendar-events-1.4.3.tgz",
"integrity": "sha1-KYBOi0TWlG5pq1ogkC2USe0xXEc="
"version": "github:jitsi/react-native-calendar-events#dabfabc4bacc1424a8b93ebdda6f3820dee198cb"
},
"react-native-callstats": {
"version": "3.27.0",
@@ -10171,9 +10454,7 @@
}
},
"react-native-fetch-blob": {
"version": "0.10.8",
"resolved": "https://registry.npmjs.org/react-native-fetch-blob/-/react-native-fetch-blob-0.10.8.tgz",
"integrity": "sha1-T8JWq64MtfEOfEHyjBGz/zMNcqk=",
"version": "github:flatfox-ag/react-native-fetch-blob#01f38a4537baecd3ea0cb93c27e84553f3fc5231",
"requires": {
"base-64": "0.1.0",
"glob": "7.0.6"
@@ -10221,9 +10502,9 @@
"integrity": "sha1-QeDsKqfdjxLzo+6Dr51jxLZw+KE="
},
"react-native-sound": {
"version": "0.10.4",
"resolved": "https://registry.npmjs.org/react-native-sound/-/react-native-sound-0.10.4.tgz",
"integrity": "sha512-V9v4CjKgv8ekQRLOJSoKA7pxJ03F4Ih3T/RfMIlMWLktz7v/O4sdJPjRBLOzZRqAnr9FWTLbSk1ZCjioXh3mjQ=="
"version": "0.10.9",
"resolved": "https://registry.npmjs.org/react-native-sound/-/react-native-sound-0.10.9.tgz",
"integrity": "sha1-awCw9K/QF83gn7udFx3xtdW4Uag="
},
"react-native-vector-icons": {
"version": "4.4.2",
@@ -10258,7 +10539,7 @@
}
},
"react-native-webrtc": {
"version": "github:jitsi/react-native-webrtc#626818af40384356617f70366133317b6a475171",
"version": "github:jitsi/react-native-webrtc#52fe4646401408e0569e972cabf08f3c21b7a107",
"requires": {
"base64-js": "1.2.3",
"event-target-shim": "1.1.1",
@@ -10294,6 +10575,14 @@
}
}
},
"react-scrolllock": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/react-scrolllock/-/react-scrolllock-2.0.2.tgz",
"integrity": "sha512-Kz0cTwHnGdTEvvNmKWKhFYrwpap8jGLbdTSjzZE1X37OJW72TVDWsL4yQ9oKS/uAsHUlbrg7AZt8Z1vgcRHmFQ==",
"requires": {
"exenv": "1.2.2"
}
},
"react-syntax-highlighter": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-3.0.2.tgz",
@@ -11467,6 +11756,47 @@
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz",
"integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew=="
},
"stdout-stream": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/stdout-stream/-/stdout-stream-1.4.0.tgz",
"integrity": "sha1-osfIWH5U2UJ+qe2zrD8s1SLfN4s=",
"dev": true,
"requires": {
"readable-stream": "2.3.6"
},
"dependencies": {
"isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=",
"dev": true
},
"readable-stream": {
"version": "2.3.6",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
"integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
"dev": true,
"requires": {
"core-util-is": "1.0.2",
"inherits": "2.0.3",
"isarray": "1.0.0",
"process-nextick-args": "2.0.0",
"safe-buffer": "5.1.1",
"string_decoder": "1.1.1",
"util-deprecate": "1.0.2"
}
},
"string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"dev": true,
"requires": {
"safe-buffer": "5.1.1"
}
}
}
},
"stream-browserify": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.1.tgz",
@@ -11947,6 +12277,30 @@
"resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz",
"integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM="
},
"true-case-path": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/true-case-path/-/true-case-path-1.0.2.tgz",
"integrity": "sha1-fskRMJJHZsf1c74wIMNPj9/QDWI=",
"dev": true,
"requires": {
"glob": "6.0.4"
},
"dependencies": {
"glob": {
"version": "6.0.4",
"resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz",
"integrity": "sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI=",
"dev": true,
"requires": {
"inflight": "1.0.6",
"inherits": "2.0.3",
"minimatch": "3.0.4",
"once": "1.4.0",
"path-is-absolute": "1.0.1"
}
}
}
},
"tsscmp": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.5.tgz",

View File

@@ -23,8 +23,8 @@
"@atlaskit/field-text-area": "1.2.0",
"@atlaskit/flag": "6.1.0",
"@atlaskit/icon": "10.0.0",
"@atlaskit/inline-dialog": "5.0.2",
"@atlaskit/inline-message": "3.0.1",
"@atlaskit/inline-dialog": "5.3.0",
"@atlaskit/inline-message": "4.0.0",
"@atlaskit/layer-manager": "2.8.0",
"@atlaskit/lozenge": "3.4.2",
"@atlaskit/modal-dialog": "3.4.0",
@@ -32,7 +32,7 @@
"@atlaskit/spinner": "4.0.0",
"@atlaskit/tabs": "4.0.1",
"@atlaskit/theme": "2.4.0",
"@atlaskit/tooltip": "6.0.0",
"@atlaskit/tooltip": "9.1.1",
"autosize": "1.18.13",
"es6-iterator": "2.0.3",
"es6-symbol": "3.1.1",
@@ -46,10 +46,9 @@
"jquery-i18next": "1.2.0",
"js-md5": "0.6.1",
"jwt-decode": "2.2.0",
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#0503ec4d3f175b154b1c6fd7037520ce0768fa58",
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#307777207ec219df344b8fed2fd2071d2238f9f8",
"lodash": "4.17.4",
"moment": "2.19.4",
"nuclear-js": "1.4.0",
"postis": "2.2.0",
"prop-types": "15.6.0",
"react": "16.2.0",
@@ -57,17 +56,17 @@
"react-i18next": "4.8.0",
"react-native": "0.51.0",
"react-native-background-timer": "2.0.0",
"react-native-calendar-events": "1.4.3",
"react-native-calendar-events": "github:jitsi/react-native-calendar-events#dabfabc4bacc1424a8b93ebdda6f3820dee198cb",
"react-native-callstats": "3.27.0",
"react-native-fetch-blob": "0.10.8",
"react-native-fetch-blob": "github:flatfox-ag/react-native-fetch-blob#exception_fixes",
"react-native-img-cache": "1.5.2",
"react-native-immersive": "1.1.0",
"react-native-keep-awake": "2.0.6",
"react-native-locale-detector": "github:jitsi/react-native-locale-detector#cc76092fc4335488a28a9529c8b50afae2c3ecdc",
"react-native-prompt": "1.0.0",
"react-native-sound": "0.10.4",
"react-native-sound": "0.10.9",
"react-native-vector-icons": "4.4.2",
"react-native-webrtc": "github:jitsi/react-native-webrtc#626818af40384356617f70366133317b6a475171",
"react-native-webrtc": "github:jitsi/react-native-webrtc#52fe4646401408e0569e972cabf08f3c21b7a107",
"react-redux": "5.0.6",
"redux": "3.7.2",
"redux-thunk": "2.2.0",
@@ -99,7 +98,7 @@
"file-loader": "1.1.5",
"flow-bin": "0.57.3",
"imports-loader": "0.7.1",
"node-sass": "3.13.1",
"node-sass": "4.8.3",
"precommit-hook": "3.0.0",
"string-replace-loader": "1.3.0",
"style-loader": "0.19.0",

View File

@@ -2,54 +2,10 @@
import React, { Component } from 'react';
import StatelessToolbar from '../toolbox/components/StatelessToolbar';
import StatelessToolbarButton
from '../toolbox/components/StatelessToolbarButton';
import ToolboxAlwaysOnTop from './ToolboxAlwaysOnTop';
const { api } = window.alwaysOnTop;
/**
* Map with toolbar button descriptors.
*/
const TOOLBAR_BUTTONS = {
/**
* The descriptor of the camera toolbar button.
*/
camera: {
classNames: [ 'button', 'icon-camera' ],
enabled: true,
id: 'toolbar_button_camera',
onClick() {
api.executeCommand('toggleVideo');
}
},
/**
* The descriptor of the toolbar button which hangs up the call/conference.
*/
hangup: {
classNames: [ 'button', 'icon-hangup', 'button_hangup' ],
enabled: true,
id: 'toolbar_button_hangup',
onClick() {
api.executeCommand('hangup');
window.close();
}
},
/**
* The descriptor of the microphone toolbar button.
*/
microphone: {
classNames: [ 'button', 'icon-microphone' ],
enabled: true,
id: 'toolbar_button_mute',
onClick() {
api.executeCommand('toggleAudio');
}
}
};
/**
* The timeout in ms for hidding the toolbar.
*/
@@ -385,62 +341,16 @@ export default class AlwaysOnTop extends Component<*, State> {
* @returns {ReactElement}
*/
render() {
const className
= `toolbar_primary always-on-top ${
this.state.visible ? 'fadeIn' : 'fadeOut'}`;
return (
<div id = 'alwaysOnTop'>
<StatelessToolbar
className = { className }
<ToolboxAlwaysOnTop
audioAvailable = { this.state.audioAvailable }
audioMuted = { this.state.audioMuted }
className = { this.state.visible ? 'fadeIn' : 'fadeOut' }
onMouseOut = { this._onMouseOut }
onMouseOver = { this._onMouseOver }>
{
Object.entries(TOOLBAR_BUTTONS).map(
([ key, button ]) => {
// XXX The following silences a couple of flow
// errors:
if (button === null
|| typeof button !== 'object') {
return null;
}
const { onClick } = button;
let enabled = false;
let toggled = false;
switch (key) {
case 'microphone':
enabled = this.state.audioAvailable;
toggled = enabled
? this.state.audioMuted : true;
break;
case 'camera':
enabled = this.state.videoAvailable;
toggled = enabled
? this.state.videoMuted : true;
break;
default: // hangup button
toggled = false;
enabled = true;
}
const updatedButton = {
...button,
enabled,
toggled
};
return (
<StatelessToolbarButton
button = { updatedButton }
key = { key }
onClick = { onClick } />
);
}
)
}
</StatelessToolbar>
onMouseOver = { this._onMouseOver }
videoAvailable = { this.state.videoAvailable }
videoMuted = { this.state.videoMuted } />
{
this._renderVideoNotAvailableScreen()
}

View File

@@ -0,0 +1,159 @@
// @flow
import React, { Component } from 'react';
// FIXME: AlwaysOnTop imports the button directly in order to avoid bringing in
// other components that use lib-jitsi-meet, which always on top does not
// import.
import ToolbarButton from '../toolbox/components/ToolbarButton';
const { api } = window.alwaysOnTop;
/**
* The type of the React {@code Component} props of {@link ToolboxAlwaysOnTop}.
*/
type Props = {
/**
* Whether or not microphone access is available.
*/
audioAvailable: boolean,
/**
* Whether or not the user is currently audio muted.
*/
audioMuted: boolean,
/**
* Additional CSS class names to add to the root of the toolbar.
*/
className: string,
/**
* Callback invoked when no longer moused over the toolbar.
*/
onMouseOut: Function,
/**
* Callback invoked when the mouse has moved over the toolbar.
*/
onMouseOver: Function,
/**
* Whether or not camera access is available.
*/
videoAvailable: boolean,
/**
* Whether or not the user is currently video muted.
*/
videoMuted: boolean
};
/**
* Represents the toolbar in the Always On Top window.
*
* @extends Component
*/
export default class ToolboxAlwaysOnTop extends Component<Props> {
/**
* Initializes a new {@code ToolboxAlwaysOnTop} instance.
*
* @param {Props} props - The read-only properties with which the new
* instance is to be initialized.
*/
constructor(props: Props) {
super(props);
// Bind event handlers so they are only bound once per instance.
this._onToolbarHangup = this._onToolbarHangup.bind(this);
this._onToolbarToggleAudio = this._onToolbarToggleAudio.bind(this);
this._onToolbarToggleVideo = this._onToolbarToggleVideo.bind(this);
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const {
audioAvailable,
audioMuted,
className = '',
onMouseOut,
onMouseOver,
videoAvailable,
videoMuted
} = this.props;
const videoMuteIcon = `${videoMuted || !videoAvailable
? 'icon-camera-disabled toggled' : 'icon-camera'} ${
videoAvailable ? '' : 'disabled'}`;
const audioMuteIcon = `${audioMuted || !audioAvailable
? 'icon-mic-disabled toggled' : 'icon-microphone'} ${
audioAvailable ? '' : 'disabled'}`;
return (
<div
className = { `always-on-top-toolbox ${className}` }
onMouseOut = { onMouseOut }
onMouseOver = { onMouseOver }>
<ToolbarButton
accessibilityLabel = 'Video mute'
iconName = { videoMuteIcon }
onClick = { this._onToolbarToggleVideo } />
<ToolbarButton
accessibilityLabel = 'Hangup'
iconName = 'icon-hangup'
onClick = { this._onToolbarHangup } />
<ToolbarButton
accessibilityLabel = 'Audio mute'
iconName = { audioMuteIcon }
onClick = { this._onToolbarToggleAudio } />
</div>
);
}
_onToolbarHangup: () => void;
/**
* Ends the conference call and closes the always on top window.
*
* @private
* @returns {void}
*/
_onToolbarHangup() {
api.executeCommand('hangup');
window.close();
}
_onToolbarToggleAudio: () => void;
/**
* Toggles audio mute if audio is avaiable.
*
* @private
* @returns {void}
*/
_onToolbarToggleAudio() {
if (this.props.audioAvailable) {
api.executeCommand('toggleAudio');
}
}
_onToolbarToggleVideo: () => void;
/**
* Toggles video mute if video is avaiable.
*
* @private
* @returns {void}
*/
_onToolbarToggleVideo() {
if (this.props.videoAvailable) {
api.executeCommand('toggleVideo');
}
}
}

View File

@@ -97,6 +97,25 @@ export function createAudioOnlyChangedEvent(enabled) {
};
}
/**
* Creates an event for an action on the deep linking page.
*
* @param {string} action - The action that the event represents.
* @param {string} actionSubject - The subject that was acted upon.
* @param {boolean} attributes - Additional attributes to attach to the event.
* @returns {Object} The event in a format suitable for sending via
* sendAnalytics.
*/
export function createDeepLinkingPageEvent(
action, actionSubject, attributes = {}) {
return {
action,
actionSubject,
source: 'deepLinkingPage',
attributes
};
}
/**
* Creates an event which indicates that a device was changed.
*
@@ -129,16 +148,21 @@ export function createFeedbackOpenEvent() {
}
/**
* Creates an event which indicates that the invite dialog was closed. This is
* not a TYPE_UI event, since it is not necessarily the result of a user
* interaction.
* Creates an event for an action regarding the AddPeopleDialog (invites).
*
* @param {string} action - The action that the event represents.
* @param {string} actionSubject - The subject that was acted upon.
* @param {boolean} attributes - Additional attributes to attach to the event.
* @returns {Object} The event in a format suitable for sending via
* sendAnalytics.
*/
export function createInviteDialogClosedEvent() {
export function createInviteDialogEvent(
action, actionSubject, attributes = {}) {
return {
action: 'invite.dialog.closed'
action,
actionSubject,
attributes,
source: 'inviteDialog'
};
}

View File

@@ -124,10 +124,8 @@ function _appNavigateToMandatoryLocation(
});
}
const profile = getState()['features/base/profile'];
return promise.then(() =>
dispatch(setConfig(_mergeConfigWithProfile(config, profile))));
dispatch(setConfig(config)));
}
}
@@ -290,23 +288,3 @@ function _loadConfig({ contextRoot, host, protocol, room }) {
throw error;
});
}
/**
* Merges the downloaded config with the current profile values. The profile
* values are named the same way as the config values in the config.js so
* a clean merge is possible.
*
* @param {Object|undefined} config - The downloaded config.
* @param {Object} profile - The persisted profile.
* @returns {Object}
*/
function _mergeConfigWithProfile(config, profile) {
if (!config) {
return;
}
return {
...config,
...profile
};
}

View File

@@ -37,6 +37,10 @@ export class App extends AbstractApp {
static propTypes = {
...AbstractApp.propTypes,
addPeopleEnabled: PropTypes.bool,
dialOutEnabled: PropTypes.bool,
/**
* Whether Picture-in-Picture is enabled. If {@code true}, a toolbar
* button is rendered in the {@link Conference} view to afford entering

View File

@@ -1,4 +1,5 @@
/* @flow */
// @flow
import { NativeModules } from 'react-native';
export * from './getRouteToRender';
@@ -11,3 +12,13 @@ export * from './getRouteToRender';
export function getName() {
return NativeModules.AppInfo.name;
}
/**
* Returns the path to the Jitsi Meet SDK bundle on iOS. On Android it will be
* undefined.
*
* @returns {string|undefined}
*/
export function getSdkBundlePath() {
return NativeModules.AppInfo.sdkBundlePath;
}

View File

@@ -1,12 +1,10 @@
/* @flow */
import { Platform } from '../base/react';
import { toState } from '../base/redux';
import { getDeepLinkingPage } from '../deep-linking';
import {
NoMobileApp,
PluginRequiredBrowser,
UnsupportedDesktopBrowser,
UnsupportedMobileBrowser
UnsupportedDesktopBrowser
} from '../unsupported-browser';
import {
@@ -24,43 +22,18 @@ declare var loggingConfig: Object;
*
* @private
* @param {Object} state - Object containing current redux state.
* @returns {ReactElement|void}
* @returns {Promise<ReactElement>|void}
* @type {Function[]}
*/
const _INTERCEPT_COMPONENT_RULES = [
/**
* This rule describes case when user opens application using mobile
* browser. In order to promote the app, we choose to suggest the mobile
* app even if the browser supports the app (e.g. Google Chrome with
* WebRTC support on Android).
*
* @param {Object} state - The redux state of the app.
* @returns {UnsupportedMobileBrowser|void} If the rule is satisfied then
* we should intercept existing component by UnsupportedMobileBrowser.
*/
// eslint-disable-next-line no-unused-vars
state => {
const OS = Platform.OS;
if (OS === 'android' || OS === 'ios') {
const mobileAppPromo
= typeof interfaceConfig === 'object'
&& interfaceConfig.MOBILE_APP_PROMO;
return (
typeof mobileAppPromo === 'undefined' || Boolean(mobileAppPromo)
? UnsupportedMobileBrowser
: NoMobileApp);
}
},
getDeepLinkingPage,
state => {
const { webRTCReady } = state['features/base/lib-jitsi-meet'];
switch (typeof webRTCReady) {
case 'boolean':
if (webRTCReady === false) {
return UnsupportedDesktopBrowser;
return Promise.resolve(UnsupportedDesktopBrowser);
}
break;
@@ -69,8 +42,10 @@ const _INTERCEPT_COMPONENT_RULES = [
break;
default:
return PluginRequiredBrowser;
return Promise.resolve(PluginRequiredBrowser);
}
return Promise.resolve();
}
];
@@ -80,16 +55,19 @@ const _INTERCEPT_COMPONENT_RULES = [
*
* @param {(Object|Function)} stateOrGetState - The redux state or
* {@link getState} function.
* @returns {Route}
* @returns {Promise<Route>}
*/
export function _getRouteToRender(stateOrGetState: Object | Function) {
export function _getRouteToRender(stateOrGetState: Object | Function): Object {
const route = _super_getRouteToRender(stateOrGetState);
// Intercepts route components if any of component interceptor rules is
// satisfied.
route.component = _interceptComponent(stateOrGetState, route.component);
return _interceptComponent(stateOrGetState, route.component).then(
(component: React$Element<*>) => {
route.component = component;
return route;
return route;
}, () => Promise.resolve(route));
}
/**
@@ -99,23 +77,24 @@ export function _getRouteToRender(stateOrGetState: Object | Function) {
* {@link getState} function.
* @param {ReactElement} component - Current route component to render.
* @private
* @returns {ReactElement} If any of the pre-defined rules is satisfied, returns
* intercepted component.
* @returns {Promise<ReactElement>} If any of the pre-defined rules is
* satisfied, returns intercepted component.
*/
function _interceptComponent(
stateOrGetState: Object | Function,
component: React$Element<*>) {
let result;
const state = toState(stateOrGetState);
for (const rule of _INTERCEPT_COMPONENT_RULES) {
result = rule(state);
if (result) {
break;
}
}
const promises = [];
return result || component;
_INTERCEPT_COMPONENT_RULES.forEach(rule => {
promises.push(rule(state));
});
return Promise.all(promises).then(
results =>
results.find(result => typeof result !== 'undefined') || component,
() => Promise.resolve(component));
}
/**

View File

@@ -79,7 +79,18 @@ function _navigate({ getState }) {
const { app } = state['features/app'];
const routeToRender = _getRouteToRender(state);
return app._navigate(routeToRender);
// XXX Web changed _getRouteToRender to return Promsie instead of Route.
// Unfortunately, the commit left mobile to return Route.
let routeToRenderPromise;
if (routeToRender && typeof routeToRender.then === 'function') {
routeToRenderPromise = routeToRender;
}
if (!routeToRenderPromise) {
routeToRenderPromise = Promise.resolve(routeToRender);
}
routeToRenderPromise.then(app._navigate.bind(app));
}
/**

View File

@@ -62,6 +62,17 @@ export const CONFERENCE_WILL_LEAVE = Symbol('CONFERENCE_WILL_LEAVE');
*/
export const DATA_CHANNEL_OPENED = Symbol('DATA_CHANNEL_OPENED');
/**
* The type of action which signals that the user has been kicked out from
* the conference.
*
* {
* type: KICKED_OUT,
* conference: JitsiConference
* }
*/
export const KICKED_OUT = Symbol('KICKED_OUT');
/**
* The type of (redux) action which signals that the lock state of a specific
* {@code JitsiConference} changed.
@@ -96,6 +107,18 @@ export const P2P_STATUS_CHANGED = Symbol('P2P_STATUS_CHANGED');
*/
export const SET_AUDIO_ONLY = Symbol('SET_AUDIO_ONLY');
/**
* The type of (redux) action which sets the desktop sharing enabled flag for
* the current conference.
*
* {
* type: SET_DESKTOP_SHARING_ENABLED,
* desktopSharingEnabled: boolean
* }
*/
export const SET_DESKTOP_SHARING_ENABLED
= Symbol('SET_DESKTOP_SHARING_ENABLED');
/**
* The type of (redux) action which updates the current known status of the
* Follow Me feature.

View File

@@ -28,9 +28,11 @@ import {
CONFERENCE_WILL_JOIN,
CONFERENCE_WILL_LEAVE,
DATA_CHANNEL_OPENED,
KICKED_OUT,
LOCK_STATE_CHANGED,
P2P_STATUS_CHANGED,
SET_AUDIO_ONLY,
SET_DESKTOP_SHARING_ENABLED,
SET_FOLLOW_ME,
SET_LASTN,
SET_PASSWORD,
@@ -77,6 +79,10 @@ function _addConferenceListeners(conference, dispatch) {
JitsiConferenceEvents.CONFERENCE_LEFT,
(...args) => dispatch(conferenceLeft(conference, ...args)));
conference.on(
JitsiConferenceEvents.KICKED,
() => dispatch(kickedOut(conference)));
conference.on(
JitsiConferenceEvents.LOCK_STATE_CHANGED,
(...args) => dispatch(lockStateChanged(conference, ...args)));
@@ -357,6 +363,23 @@ export function dataChannelOpened() {
};
}
/**
* Signals that we've been kicked out of the conference.
*
* @param {JitsiConference} conference - The {@link JitsiConference} instance
* for which the event is being signaled.
* @returns {{
* type: KICKED_OUT,
* conference: JitsiConference
* }}
*/
export function kickedOut(conference: Object) {
return {
type: KICKED_OUT,
conference
};
}
/**
* Signals that the lock state of a specific JitsiConference changed.
*
@@ -433,6 +456,22 @@ export function setAudioOnly(audioOnly: boolean) {
};
}
/**
* Sets the flag for indicating if desktop sharing is enabled.
*
* @param {boolean} desktopSharingEnabled - True if desktop sharing is enabled.
* @returns {{
* type: SET_DESKTOP_SHARING_ENABLED,
* desktopSharingEnabled: boolean
* }}
*/
export function setDesktopSharingEnabled(desktopSharingEnabled: boolean) {
return {
type: SET_DESKTOP_SHARING_ENABLED,
desktopSharingEnabled
};
}
/**
* Enables or disables the Follow Me feature.
*

View File

@@ -22,14 +22,11 @@ import { TRACK_ADDED, TRACK_REMOVED } from '../tracks';
import {
conferenceLeft,
createConference,
setAudioOnly,
setLastN,
toggleAudioOnly
} from './actions';
import {
CONFERENCE_FAILED,
CONFERENCE_JOINED,
CONFERENCE_LEFT,
DATA_CHANNEL_OPENED,
SET_AUDIO_ONLY,
SET_LASTN,
@@ -58,10 +55,6 @@ MiddlewareRegistry.register(store => next => action => {
case CONNECTION_ESTABLISHED:
return _connectionEstablished(store, next, action);
case CONFERENCE_FAILED:
case CONFERENCE_LEFT:
return _conferenceFailedOrLeft(store, next, action);
case CONFERENCE_JOINED:
return _conferenceJoined(store, next, action);
@@ -116,43 +109,6 @@ function _connectionEstablished({ dispatch }, next, action) {
return result;
}
/**
* Does extra sync up on properties that may need to be updated after the
* conference failed or was left.
*
* @param {Store} store - The redux store in which the specified action is being
* dispatched.
* @param {Dispatch} next - The redux dispatch function to dispatch the
* specified action to the specified store.
* @param {Action} action - The redux action {@link CONFERENCE_FAILED} or
* {@link CONFERENCE_LEFT} which is being dispatched in the specified store.
* @private
* @returns {Object} The value returned by {@code next(action)}.
*/
function _conferenceFailedOrLeft({ dispatch, getState }, next, action) {
const result = next(action);
const state = getState();
const { audioOnly } = state['features/base/conference'];
const { startAudioOnly } = state['features/base/profile'];
// FIXME: Consider implementing a standalone audio-only feature that handles
// all these state changes.
if (audioOnly) {
if (!startAudioOnly) {
sendAnalytics(createAudioOnlyChangedEvent(false));
logger.log('Audio only disabled');
dispatch(setAudioOnly(false));
}
} else if (startAudioOnly) {
sendAnalytics(createAudioOnlyChangedEvent(true));
logger.log('Audio only enabled');
dispatch(setAudioOnly(true));
}
return result;
}
/**
* Does extra sync up on properties that may need to be updated after the
* conference was joined.
@@ -262,25 +218,34 @@ function _pinParticipant({ getState }, next, action) {
* @returns {Object} The value returned by {@code next(action)}.
*/
function _setAudioOnly({ dispatch, getState }, next, action) {
const { audioOnly: oldValue } = getState()['features/base/conference'];
const result = next(action);
const { audioOnly: newValue } = getState()['features/base/conference'];
const { audioOnly } = getState()['features/base/conference'];
// Send analytics. We could've done it in the action creator setAudioOnly.
// I don't know why it has to happen as early as possible but the analytics
// were originally sent before the SET_AUDIO_ONLY action was even dispatched
// in the redux store so I'm now sending the analytics as early as possible.
if (oldValue !== newValue) {
sendAnalytics(createAudioOnlyChangedEvent(newValue));
logger.log(`Audio-only ${newValue ? 'enabled' : 'disabled'}`);
}
// Set lastN to 0 in case audio-only is desired; leave it as undefined,
// otherwise, and the default lastN value will be chosen automatically.
dispatch(setLastN(audioOnly ? 0 : undefined));
dispatch(setLastN(newValue ? 0 : undefined));
// Mute/unmute the local video.
dispatch(
setVideoMuted(
audioOnly,
newValue,
VIDEO_MUTISM_AUTHORITY.AUDIO_ONLY,
/* ensureTrack */ true));
if (typeof APP !== 'undefined') {
// TODO This should be a temporary solution that lasts only until
// video tracks and all ui is moved into react/redux on the web.
APP.UI.emitEvent(UIEvents.TOGGLE_AUDIO_ONLY, audioOnly);
APP.UI.emitEvent(UIEvents.TOGGLE_AUDIO_ONLY, newValue);
}
return result;

View File

@@ -14,6 +14,7 @@ import {
LOCK_STATE_CHANGED,
P2P_STATUS_CHANGED,
SET_AUDIO_ONLY,
SET_DESKTOP_SHARING_ENABLED,
SET_FOLLOW_ME,
SET_PASSWORD,
SET_RECEIVE_VIDEO_QUALITY,
@@ -57,6 +58,9 @@ ReducerRegistry.register('features/base/conference', (state = {}, action) => {
case SET_AUDIO_ONLY:
return _setAudioOnly(state, action);
case SET_DESKTOP_SHARING_ENABLED:
return _setDesktopSharingEnabled(state, action);
case SET_FOLLOW_ME:
return {
...state,
@@ -329,6 +333,21 @@ function _setAudioOnly(state, action) {
return set(state, 'audioOnly', action.audioOnly);
}
/**
* Reduces a specific Redux action SET_DESKTOP_SHARING_ENABLED of the feature
* base/conference.
*
* @param {Object} state - The Redux state of the feature base/conference.
* @param {Action} action - The Redux action SET_DESKTOP_SHARING_ENABLED to
* reduce.
* @private
* @returns {Object} The new state of the feature base/conference after the
* reduction of the specified action.
*/
function _setDesktopSharingEnabled(state, action) {
return set(state, 'desktopSharingEnabled', action.desktopSharingEnabled);
}
/**
* Reduces a specific Redux action SET_PASSWORD of the feature base/conference.
*

View File

@@ -20,7 +20,6 @@ const WHITELISTED_KEYS = [
'_peerConnStatusRtcMuteTimeout',
'abTesting',
'alwaysVisibleToolbar',
'autoEnableDesktopSharing',
'autoRecord',
'autoRecordToken',
'avgRtpStatsN',
@@ -57,20 +56,24 @@ const WHITELISTED_KEYS = [
'enableLipSync',
'enableLocalVideoFlip',
'enableRecording',
'enableRemb',
'enableStatsID',
'enableTalkWhileMuted',
'enableTcc',
'enableUserRolesBasedOnToken',
'etherpad_base',
'failICE',
'firefox_fake_device',
'forceJVB121Ratio',
'gatherStats',
'googleApiApplicationClientID',
'hiddenDomain',
'hosts',
'iAmRecorder',
'iAmSipGateway',
'iceTransportPolicy',
'ignoreStartMuted',
'minParticipants',
'nick',
'openBridgeChannel',
'p2p',

View File

@@ -1,4 +1,4 @@
/* @flow */
// @flow
import type { Dispatch } from 'redux';
@@ -30,34 +30,38 @@ export function connect() {
// XXX Lib-jitsi-meet does not accept uppercase letters.
const room = state['features/base/conference'].room.toLowerCase();
const { initPromise } = state['features/base/lib-jitsi-meet'];
// XXX For web based version we use conference initialization logic
// from the old app (at the moment of writing).
return APP.conference.init({ roomName: room })
.catch(error => {
APP.API.notifyConferenceLeft(APP.conference.roomName);
logger.error(error);
return initPromise.then(() => APP.conference.init({
roomName: room
})).catch(error => {
APP.API.notifyConferenceLeft(APP.conference.roomName);
logger.error(error);
// TODO The following are in fact Errors raised by
// JitsiMeetJS.init() which should be taken care of in
// features/base/lib-jitsi-meet but we are not there yet on the
// Web at the time of this writing.
switch (error.name) {
case WEBRTC_NOT_READY:
case WEBRTC_NOT_SUPPORTED:
dispatch(libInitError(error));
}
});
// TODO The following are in fact Errors raised by
// JitsiMeetJS.init() which should be taken care of in
// features/base/lib-jitsi-meet but we are not there yet on the
// Web at the time of this writing.
switch (error.name) {
case WEBRTC_NOT_READY:
case WEBRTC_NOT_SUPPORTED:
dispatch(libInitError(error));
}
});
};
}
/**
* Closes connection.
*
* @param {boolean} [requestFeedback] - Whether or not to attempt showing a
* request for call feedback.
* @returns {Function}
*/
export function disconnect() {
export function disconnect(requestFeedback: boolean = false) {
// XXX For web based version we use conference hanging up logic from the old
// app.
return () => APP.conference.hangup();
return () => APP.conference.hangup(requestFeedback);
}

View File

@@ -52,6 +52,24 @@ export function getURLWithoutParams(url: URL): URL {
return url;
}
/**
* Gets a URL string without hash and query/search params from a specific
* {@code URL}.
*
* @param {URL} url - The {@code URL} which may have hash and query/search
* params.
* @returns {string}
*/
export function getURLWithoutParamsNormalized(url: URL): string {
const urlWithoutParams = getURLWithoutParams(url).href;
if (urlWithoutParams) {
return urlWithoutParams.toLowerCase();
}
return '';
}
/**
* Converts a specific id to jid if it's not jid yet.
*

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