fix(largeVideo): update don't depend on thumbnails
This commit is contained in:
parent
f972ebfe9e
commit
4fda428be1
|
@ -19,7 +19,6 @@ import {
|
||||||
createDeviceChangedEvent,
|
createDeviceChangedEvent,
|
||||||
createStartSilentEvent,
|
createStartSilentEvent,
|
||||||
createScreenSharingEvent,
|
createScreenSharingEvent,
|
||||||
createStreamSwitchDelayEvent,
|
|
||||||
createTrackMutedEvent,
|
createTrackMutedEvent,
|
||||||
sendAnalytics
|
sendAnalytics
|
||||||
} from './react/features/analytics';
|
} from './react/features/analytics';
|
||||||
|
@ -2263,18 +2262,6 @@ export default {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
/* eslint-disable max-params */
|
|
||||||
APP.UI.addListener(
|
|
||||||
UIEvents.RESOLUTION_CHANGED,
|
|
||||||
(id, oldResolution, newResolution, delay) => {
|
|
||||||
sendAnalytics(createStreamSwitchDelayEvent(
|
|
||||||
{
|
|
||||||
'old_resolution': oldResolution,
|
|
||||||
'new_resolution': newResolution,
|
|
||||||
value: delay
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
|
|
||||||
APP.UI.addListener(UIEvents.AUTH_CLICKED, () => {
|
APP.UI.addListener(UIEvents.AUTH_CLICKED, () => {
|
||||||
AuthHandler.authenticate(room);
|
AuthHandler.authenticate(room);
|
||||||
});
|
});
|
||||||
|
|
|
@ -322,15 +322,6 @@ UI.inputDisplayNameHandler = function(newDisplayName) {
|
||||||
eventEmitter.emit(UIEvents.NICKNAME_CHANGED, newDisplayName);
|
eventEmitter.emit(UIEvents.NICKNAME_CHANGED, newDisplayName);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the type of the remote video.
|
|
||||||
* @param jid the jid for the remote video
|
|
||||||
* @returns the video type video or screen.
|
|
||||||
*/
|
|
||||||
UI.getRemoteVideoType = function(jid) {
|
|
||||||
return VideoLayout.getRemoteVideoType(jid);
|
|
||||||
};
|
|
||||||
|
|
||||||
// FIXME check if someone user this
|
// FIXME check if someone user this
|
||||||
UI.showLoginPopup = function(callback) {
|
UI.showLoginPopup = function(callback) {
|
||||||
logger.log('password is required');
|
logger.log('password is required');
|
||||||
|
|
|
@ -19,7 +19,6 @@ export default class SharedVideoThumb extends SmallVideo {
|
||||||
this.id = participant.id;
|
this.id = participant.id;
|
||||||
this.isLocal = false;
|
this.isLocal = false;
|
||||||
this.url = participant.id;
|
this.url = participant.id;
|
||||||
this.setVideoType(videoType);
|
|
||||||
this.videoSpanId = 'sharedVideoContainer';
|
this.videoSpanId = 'sharedVideoContainer';
|
||||||
this.container = this.createContainer(this.videoSpanId);
|
this.container = this.createContainer(this.videoSpanId);
|
||||||
this.$container = $(this.container);
|
this.$container = $(this.container);
|
||||||
|
|
|
@ -481,10 +481,6 @@ export default class RemoteVideo extends SmallVideo {
|
||||||
|
|
||||||
isVideo ? this.videoStream = stream : this.audioStream = stream;
|
isVideo ? this.videoStream = stream : this.audioStream = stream;
|
||||||
|
|
||||||
if (isVideo) {
|
|
||||||
this.setVideoType(stream.videoType);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!stream.getOriginalStream()) {
|
if (!stream.getOriginalStream()) {
|
||||||
logger.debug('Remote video stream has no original stream');
|
logger.debug('Remote video stream has no original stream');
|
||||||
|
|
||||||
|
|
|
@ -32,8 +32,6 @@ import {
|
||||||
|
|
||||||
const logger = require('jitsi-meet-logger').getLogger(__filename);
|
const logger = require('jitsi-meet-logger').getLogger(__filename);
|
||||||
|
|
||||||
import UIEvents from '../../../service/UI/UIEvents';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Display mode constant used when video is being displayed on the small video.
|
* Display mode constant used when video is being displayed on the small video.
|
||||||
* @type {number}
|
* @type {number}
|
||||||
|
@ -158,28 +156,6 @@ export default class SmallVideo {
|
||||||
return this.$container.is(':visible');
|
return this.$container.is(':visible');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the type of the video displayed by this instance.
|
|
||||||
* Note that this is a string without clearly defined or checked values, and
|
|
||||||
* it is NOT one of the strings defined in service/RTC/VideoType in
|
|
||||||
* lib-jitsi-meet.
|
|
||||||
* @param videoType 'camera' or 'desktop', or 'sharedvideo'.
|
|
||||||
*/
|
|
||||||
setVideoType(videoType) {
|
|
||||||
this.videoType = videoType;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the type of the video displayed by this instance.
|
|
||||||
* Note that this is a string without clearly defined or checked values, and
|
|
||||||
* it is NOT one of the strings defined in service/RTC/VideoType in
|
|
||||||
* lib-jitsi-meet.
|
|
||||||
* @returns {String} 'camera', 'screen', 'sharedvideo', or undefined.
|
|
||||||
*/
|
|
||||||
getVideoType() {
|
|
||||||
return this.videoType;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates an audio or video element for a particular MediaStream.
|
* Creates an audio or video element for a particular MediaStream.
|
||||||
*/
|
*/
|
||||||
|
@ -452,7 +428,7 @@ export default class SmallVideo {
|
||||||
* or <tt>false</tt> otherwise.
|
* or <tt>false</tt> otherwise.
|
||||||
*/
|
*/
|
||||||
isCurrentlyOnLargeVideo() {
|
isCurrentlyOnLargeVideo() {
|
||||||
return this.VideoLayout.isCurrentlyOnLarge(this.id);
|
return APP.store.getState()['features/large-video']?.participantId === this.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -640,39 +616,6 @@ export default class SmallVideo {
|
||||||
this.updateIndicators();
|
this.updateIndicators();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds a listener for onresize events for this video, which will monitor for
|
|
||||||
* resolution changes, will calculate the delay since the moment the listened
|
|
||||||
* is added, and will fire a RESOLUTION_CHANGED event.
|
|
||||||
*/
|
|
||||||
waitForResolutionChange() {
|
|
||||||
const beforeChange = window.performance.now();
|
|
||||||
const videos = this.selectVideoElement();
|
|
||||||
|
|
||||||
if (!videos || !videos.length || videos.length <= 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const video = videos[0];
|
|
||||||
const oldWidth = video.videoWidth;
|
|
||||||
const oldHeight = video.videoHeight;
|
|
||||||
|
|
||||||
video.onresize = () => {
|
|
||||||
// eslint-disable-next-line eqeqeq
|
|
||||||
if (video.videoWidth != oldWidth || video.videoHeight != oldHeight) {
|
|
||||||
// Only run once.
|
|
||||||
video.onresize = null;
|
|
||||||
|
|
||||||
const delay = window.performance.now() - beforeChange;
|
|
||||||
const emitter = this.VideoLayout.getEventEmitter();
|
|
||||||
|
|
||||||
if (emitter) {
|
|
||||||
emitter.emit(UIEvents.RESOLUTION_CHANGED, this.getId(), `${oldWidth}x${oldHeight}`,
|
|
||||||
`${video.videoWidth}x${video.videoHeight}`, delay);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initalizes any browser specific properties. Currently sets the overflow
|
* Initalizes any browser specific properties. Currently sets the overflow
|
||||||
* property for Qt browsers on Windows to hidden, thus fixing the following
|
* property for Qt browsers on Windows to hidden, thus fixing the following
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
/* global APP, $, interfaceConfig */
|
/* global APP, $, interfaceConfig */
|
||||||
const logger = require('jitsi-meet-logger').getLogger(__filename);
|
const logger = require('jitsi-meet-logger').getLogger(__filename);
|
||||||
|
|
||||||
import { VIDEO_TYPE } from '../../../react/features/base/media';
|
import { MEDIA_TYPE, VIDEO_TYPE } from '../../../react/features/base/media';
|
||||||
import {
|
import {
|
||||||
getLocalParticipant as getLocalParticipantFromStore,
|
getLocalParticipant as getLocalParticipantFromStore,
|
||||||
getPinnedParticipant,
|
getPinnedParticipant,
|
||||||
|
getParticipantById,
|
||||||
pinParticipant
|
pinParticipant
|
||||||
} from '../../../react/features/base/participants';
|
} from '../../../react/features/base/participants';
|
||||||
|
import { getTrackByMediaTypeAndParticipant } from '../../../react/features/base/tracks';
|
||||||
import { SHARED_VIDEO_CONTAINER_TYPE } from '../shared_video/SharedVideo';
|
import { SHARED_VIDEO_CONTAINER_TYPE } from '../shared_video/SharedVideo';
|
||||||
import SharedVideoThumb from '../shared_video/SharedVideoThumb';
|
import SharedVideoThumb from '../shared_video/SharedVideoThumb';
|
||||||
|
|
||||||
|
@ -73,9 +75,6 @@ const VideoLayout = {
|
||||||
emitter,
|
emitter,
|
||||||
this._updateLargeVideoIfDisplayed.bind(this));
|
this._updateLargeVideoIfDisplayed.bind(this));
|
||||||
|
|
||||||
// sets default video type of local video
|
|
||||||
// FIXME container type is totally different thing from the video type
|
|
||||||
localVideoThumbnail.setVideoType(VIDEO_CONTAINER_TYPE);
|
|
||||||
this.registerListeners();
|
this.registerListeners();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -221,10 +220,16 @@ const VideoLayout = {
|
||||||
* @returns {String} the video type video or screen.
|
* @returns {String} the video type video or screen.
|
||||||
*/
|
*/
|
||||||
getRemoteVideoType(id) {
|
getRemoteVideoType(id) {
|
||||||
const smallVideo = VideoLayout.getSmallVideo(id);
|
const state = APP.store.getState();
|
||||||
|
const participant = getParticipantById(state, id);
|
||||||
|
|
||||||
|
if (participant?.isFakeParticipant) {
|
||||||
|
return SHARED_VIDEO_CONTAINER_TYPE;
|
||||||
|
}
|
||||||
|
|
||||||
return smallVideo ? smallVideo.getVideoType() : null;
|
const videoTrack = getTrackByMediaTypeAndParticipant(state['features/base/tracks'], MEDIA_TYPE.VIDEO, id);
|
||||||
|
|
||||||
|
return videoTrack?.videoType;
|
||||||
},
|
},
|
||||||
|
|
||||||
isPinned(id) {
|
isPinned(id) {
|
||||||
|
@ -308,12 +313,6 @@ const VideoLayout = {
|
||||||
addRemoteVideoContainer(id, remoteVideo) {
|
addRemoteVideoContainer(id, remoteVideo) {
|
||||||
remoteVideos[id] = remoteVideo;
|
remoteVideos[id] = remoteVideo;
|
||||||
|
|
||||||
if (!remoteVideo.getVideoType()) {
|
|
||||||
// make video type the default one (camera)
|
|
||||||
// FIXME container type is not a video type
|
|
||||||
remoteVideo.setVideoType(VIDEO_CONTAINER_TYPE);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize the view
|
// Initialize the view
|
||||||
remoteVideo.updateView();
|
remoteVideo.updateView();
|
||||||
},
|
},
|
||||||
|
@ -491,22 +490,6 @@ const VideoLayout = {
|
||||||
|
|
||||||
logger.info('Peer video type changed: ', id, newVideoType);
|
logger.info('Peer video type changed: ', id, newVideoType);
|
||||||
|
|
||||||
let smallVideo;
|
|
||||||
|
|
||||||
if (APP.conference.isLocalId(id)) {
|
|
||||||
if (!localVideoThumbnail) {
|
|
||||||
logger.warn('Local video not ready yet');
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
smallVideo = localVideoThumbnail;
|
|
||||||
} else if (remoteVideos[id]) {
|
|
||||||
smallVideo = remoteVideos[id];
|
|
||||||
} else {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
smallVideo.setVideoType(newVideoType);
|
|
||||||
|
|
||||||
this._updateLargeVideoIfDisplayed(id, true);
|
this._updateLargeVideoIfDisplayed(id, true);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -584,17 +567,16 @@ const VideoLayout = {
|
||||||
}
|
}
|
||||||
const currentContainer = largeVideo.getCurrentContainer();
|
const currentContainer = largeVideo.getCurrentContainer();
|
||||||
const currentContainerType = largeVideo.getCurrentContainerType();
|
const currentContainerType = largeVideo.getCurrentContainerType();
|
||||||
const currentId = largeVideo.id;
|
|
||||||
const isOnLarge = this.isCurrentlyOnLarge(id);
|
const isOnLarge = this.isCurrentlyOnLarge(id);
|
||||||
const smallVideo = this.getSmallVideo(id);
|
const state = APP.store.getState();
|
||||||
|
const videoTrack = getTrackByMediaTypeAndParticipant(state['features/base/tracks'], MEDIA_TYPE.VIDEO, id);
|
||||||
|
const videoStream = videoTrack?.jitsiTrack;
|
||||||
|
|
||||||
if (isOnLarge && !forceUpdate
|
if (isOnLarge && !forceUpdate
|
||||||
&& LargeVideoManager.isVideoContainer(currentContainerType)
|
&& LargeVideoManager.isVideoContainer(currentContainerType)
|
||||||
&& smallVideo) {
|
&& videoStream) {
|
||||||
const currentStreamId = currentContainer.getStreamID();
|
const currentStreamId = currentContainer.getStreamID();
|
||||||
const newStreamId
|
const newStreamId = videoStream?.getId() || null;
|
||||||
= smallVideo.videoStream
|
|
||||||
? smallVideo.videoStream.getId() : null;
|
|
||||||
|
|
||||||
// FIXME it might be possible to get rid of 'forceUpdate' argument
|
// FIXME it might be possible to get rid of 'forceUpdate' argument
|
||||||
if (currentStreamId !== newStreamId) {
|
if (currentStreamId !== newStreamId) {
|
||||||
|
@ -603,42 +585,17 @@ const VideoLayout = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((!isOnLarge || forceUpdate) && smallVideo) {
|
if (!isOnLarge || forceUpdate) {
|
||||||
const videoType = this.getRemoteVideoType(id);
|
const videoType = this.getRemoteVideoType(id);
|
||||||
|
|
||||||
// FIXME video type is not the same thing as container type
|
|
||||||
|
|
||||||
if (id !== currentId && videoType === VIDEO_CONTAINER_TYPE) {
|
|
||||||
APP.API.notifyOnStageParticipantChanged(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
let oldSmallVideo;
|
|
||||||
|
|
||||||
if (currentId) {
|
|
||||||
oldSmallVideo = this.getSmallVideo(currentId);
|
|
||||||
}
|
|
||||||
|
|
||||||
smallVideo.waitForResolutionChange();
|
|
||||||
if (oldSmallVideo) {
|
|
||||||
oldSmallVideo.waitForResolutionChange();
|
|
||||||
}
|
|
||||||
|
|
||||||
largeVideo.updateLargeVideo(
|
largeVideo.updateLargeVideo(
|
||||||
id,
|
id,
|
||||||
smallVideo.videoStream,
|
videoStream,
|
||||||
videoType || VIDEO_TYPE.CAMERA
|
videoType || VIDEO_TYPE.CAMERA
|
||||||
).then(() => {
|
).catch(() => {
|
||||||
// update current small video and the old one
|
// do nothing
|
||||||
smallVideo.updateView();
|
|
||||||
oldSmallVideo && oldSmallVideo.updateView();
|
|
||||||
}, () => {
|
|
||||||
// use clicked other video during update, nothing to do.
|
|
||||||
});
|
});
|
||||||
|
|
||||||
} else if (currentId) {
|
|
||||||
const currentSmallVideo = this.getSmallVideo(currentId);
|
|
||||||
|
|
||||||
currentSmallVideo && currentSmallVideo.updateView();
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -723,10 +680,6 @@ const VideoLayout = {
|
||||||
this.localFlipX = val;
|
this.localFlipX = val;
|
||||||
},
|
},
|
||||||
|
|
||||||
getEventEmitter() {
|
|
||||||
return eventEmitter;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles user's features changes.
|
* Handles user's features changes.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -688,21 +688,6 @@ export function createStartMutedConfigurationEvent(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates an event which indicates the delay for switching between simulcast
|
|
||||||
* streams.
|
|
||||||
*
|
|
||||||
* @param {Object} attributes - Attributes to attach to the event.
|
|
||||||
* @returns {Object} The event in a format suitable for sending via
|
|
||||||
* sendAnalytics.
|
|
||||||
*/
|
|
||||||
export function createStreamSwitchDelayEvent(attributes) {
|
|
||||||
return {
|
|
||||||
action: 'stream.switch.delay',
|
|
||||||
attributes
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Automatically changing the mute state of a media track in order to match
|
* Automatically changing the mute state of a media track in order to match
|
||||||
* the current stored state in redux.
|
* the current stored state in redux.
|
||||||
|
|
|
@ -129,6 +129,8 @@ MiddlewareRegistry.register(store => next => action => {
|
||||||
// TODO Remove the following calls to APP.UI once components interested
|
// TODO Remove the following calls to APP.UI once components interested
|
||||||
// in track mute changes are moved into React and/or redux.
|
// in track mute changes are moved into React and/or redux.
|
||||||
if (typeof APP !== 'undefined') {
|
if (typeof APP !== 'undefined') {
|
||||||
|
const result = next(action);
|
||||||
|
|
||||||
const { jitsiTrack } = action.track;
|
const { jitsiTrack } = action.track;
|
||||||
const muted = jitsiTrack.isMuted();
|
const muted = jitsiTrack.isMuted();
|
||||||
const participantID = jitsiTrack.getParticipantId();
|
const participantID = jitsiTrack.getParticipantId();
|
||||||
|
@ -151,6 +153,8 @@ MiddlewareRegistry.register(store => next => action => {
|
||||||
} else {
|
} else {
|
||||||
APP.UI.setAudioMuted(participantID, muted);
|
APP.UI.setAudioMuted(participantID, muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
|
import { MEDIA_TYPE, VIDEO_TYPE } from '../base/media';
|
||||||
import { getLocalParticipant } from '../base/participants';
|
import { getLocalParticipant } from '../base/participants';
|
||||||
import { StateListenerRegistry } from '../base/redux';
|
import { StateListenerRegistry } from '../base/redux';
|
||||||
|
import { getTrackByMediaTypeAndParticipant } from '../base/tracks';
|
||||||
import { appendSuffix } from '../display-name';
|
import { appendSuffix } from '../display-name';
|
||||||
import { shouldDisplayTileView } from '../video-layout';
|
import { shouldDisplayTileView } from '../video-layout';
|
||||||
|
|
||||||
|
@ -37,3 +39,18 @@ StateListenerRegistry.register(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the on stage participant value.
|
||||||
|
*/
|
||||||
|
StateListenerRegistry.register(
|
||||||
|
/* selector */ state => state['features/large-video'].participantId,
|
||||||
|
/* listener */ (participantId, store) => {
|
||||||
|
const videoTrack = getTrackByMediaTypeAndParticipant(
|
||||||
|
store.getState()['features/base/tracks'], MEDIA_TYPE.VIDEO, participantId);
|
||||||
|
|
||||||
|
if (videoTrack && videoTrack.videoType === VIDEO_TYPE.CAMERA) {
|
||||||
|
APP.API.notifyOnStageParticipantChanged(participantId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
import { StateListenerRegistry, equals } from '../base/redux';
|
import { StateListenerRegistry, equals } from '../base/redux';
|
||||||
import Filmstrip from '../../../modules/UI/videolayout/Filmstrip';
|
import Filmstrip from '../../../modules/UI/videolayout/Filmstrip';
|
||||||
|
import VideoLayout from '../../../modules/UI/videolayout/VideoLayout';
|
||||||
import { getCurrentLayout, getTileViewGridDimensions, shouldDisplayTileView, LAYOUTS } from '../video-layout';
|
import { getCurrentLayout, getTileViewGridDimensions, shouldDisplayTileView, LAYOUTS } from '../video-layout';
|
||||||
|
|
||||||
import { setHorizontalViewDimensions, setTileViewDimensions } from './actions';
|
import { setHorizontalViewDimensions, setTileViewDimensions } from './actions';
|
||||||
|
@ -56,3 +57,22 @@ StateListenerRegistry.register(
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles on stage participant updates.
|
||||||
|
*/
|
||||||
|
StateListenerRegistry.register(
|
||||||
|
/* selector */ state => state['features/large-video'].participantId,
|
||||||
|
/* listener */ (participantId, store, oldParticipantId) => {
|
||||||
|
const newThumbnail = VideoLayout.getSmallVideo(participantId);
|
||||||
|
const oldThumbnail = VideoLayout.getSmallVideo(oldParticipantId);
|
||||||
|
|
||||||
|
if (newThumbnail) {
|
||||||
|
newThumbnail.updateView();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oldThumbnail) {
|
||||||
|
oldThumbnail.updateView();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
|
@ -53,10 +53,6 @@ export default {
|
||||||
*/
|
*/
|
||||||
LOCAL_FLIPX_CHANGED: 'UI.local_flipx_changed',
|
LOCAL_FLIPX_CHANGED: 'UI.local_flipx_changed',
|
||||||
|
|
||||||
// An event which indicates that the resolution of a remote video has
|
|
||||||
// changed.
|
|
||||||
RESOLUTION_CHANGED: 'UI.resolution_changed',
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Notifies that the button "Cancel" is pressed on the dialog for
|
* Notifies that the button "Cancel" is pressed on the dialog for
|
||||||
* external extension installation.
|
* external extension installation.
|
||||||
|
|
Loading…
Reference in New Issue