css-loader

pull/1102/head
Vincent 5 years ago
parent 54b7d9a21b
commit c26e20d33f

@ -1930,7 +1930,7 @@
toastOptions.title = i18n('youLeftTheGroup');
toastOptions.id = 'youLeftTheGroup';
}
if (message.length > window.CONSTANTS.MAX_MESSAGE_BODY_LENGTH) {
if (message.length > window.libsession.Constants.CONVERSATION.MAX_MESSAGE_BODY_LENGTH) {
toastOptions.title = i18n('messageBodyTooLong');
toastOptions.id = 'messageBodyTooLong';
}

@ -161,6 +161,7 @@
"bower": "1.8.2",
"chai": "4.1.2",
"chai-as-promised": "^7.1.1",
"css-loader": "^3.6.0",
"dashdash": "1.14.1",
"electron": "8.2.0",
"electron-builder": "22.3.6",

@ -84,7 +84,6 @@ window.CONSTANTS = new (function() {
this.DEFAULT_PUBLIC_CHAT_URL = appConfig.get('defaultPublicChatServer');
this.MAX_LINKED_DEVICES = 1;
this.MAX_CONNECTION_DURATION = 5000;
this.MAX_MESSAGE_BODY_LENGTH = 64 * 1024;
// Limited due to the proof-of-work requirement
this.SMALL_GROUP_SIZE_LIMIT = 10;
// Number of seconds to turn on notifications after reconnect/start of app
@ -102,25 +101,6 @@ window.CONSTANTS = new (function() {
2}}[a-zA-Z0-9_]){0,1}$`;
this.MIN_GUARD_COUNT = 2;
this.DESIRED_GUARD_COUNT = 3;
// ///////////////////////// //
// User Interface //
// //////////////////////// //
this.MAX_MESSAGE_BODY_LENGTH = 2000;
// Limited due to the proof-of-work requirement
this.DEFAULT_MEDIA_FETCH_COUNT = 50;
this.DEFAULT_DOCUMENTS_FETCH_COUNT = 150;
this.DEFAULT_MESSAGE_FETCH_COUNT = 30;
this.MAX_MESSAGE_FETCH_COUNT = 500;
// Pixels (scroll) from the top of the top of message container
// at which more messages should be loaded
this.MESSAGE_CONTAINER_BUFFER_OFFSET_PX = 30;
this.MESSAGE_FETCH_INTERVAL = 1;
// Maximum voice message duraiton of 5 minutes
// which equates to 1.97 MB
this.MAX_VOICE_MESSAGE_DURATION = 300;
// Max attachment size: 10 MB
this.MAX_ATTACHMENT_FILESIZE = 10000000;
})();
window.versionInfo = {

@ -389,7 +389,7 @@ $session-element-border-green: 4px solid $session-color-green;
.module-message__author-avatar {
display: inline-flex;
margin-right: 20px;
padding-top: 5px;
margin-top: 3px;
}
.module-message__container {

@ -171,6 +171,18 @@ $composition-container-height: 60px;
}
}
.session-message-wrapper {
font-family: $session-font-default;
letter-spacing: 0.03em;
margin-top: 3px;
margin-bottom: 3px;
.react-contextmenu-wrapper {
display: flex;
align-items: start;
}
}
.composition-container {
display: flex;
justify-content: center;
@ -402,3 +414,312 @@ $composition-container-height: 60px;
}
}
}
/* ************ */
/* AUDIO PLAYER */
/* ************ */
$rhap_theme-color: #212121 !default;
$rhap_background-color: rgba(0,0,0,0) !default;
$rhap_bar-color: #232323 !default;
$rhap_time-color: #DDDDDD !default;
$rhap_font-family: inherit !default;
.rhap_container, .rhap_container button, .rhap_progress-container {
outline: none;
}
.rhap_container {
box-sizing: border-box;
display: flex;
flex-direction: column;
line-height: 1;
font-family: $rhap_font-family;
min-width: 220px;
padding: 10px 0px;
&:focus:not(:focus-visible) {
outline: 0;
}
svg {
vertical-align: initial; // overwrite Bootstrap default
}
}
.rhap_current-time {
display: none;
}
.rhap_total-time{
margin-left: 10px;
}
.rhap_play-pause-button {
display: flex;
justify-content: center;
align-items: center;
}
.rhap_volume-bar {
display: none;
}
.rhap_volume-container div[role="progressbar"] {
display: none;
}
.rhap_header {
margin-bottom: 10px;
}
.rhap_footer {
margin-top: 5px;
}
.rhap_main {
display: flex;
flex-direction: column;
flex: 1 1 auto;
}
.rhap_stacked {
.rhap_controls-section {
margin-top: 8px;
}
}
.rhap_horizontal {
flex-direction: row;
.rhap_controls-section {
margin-left: 8px;
}
}
.rhap_horizontal-reverse {
flex-direction: row-reverse;
.rhap_controls-section {
margin-right: 8px;
}
}
.rhap_stacked-reverse {
flex-direction: column-reverse;
.rhap_controls-section {
margin-bottom: 8px;
}
}
.rhap_progress-section {
display: flex;
flex: 3 1 auto;
align-items: center;
}
.rhap_progress-container {
display: flex;
align-items: center;
height: 20px;
flex: 1 0 auto;
align-self: center;
margin: 0 calc(10px + 1%);
cursor: pointer;
-webkit-user-select: none;
&:focus:not(:focus-visible) {
outline: 0;
}
}
.rhap_time {
color: $rhap_time-color;
font-size: 12px;
user-select: none;
-webkit-user-select: none;
}
.rhap_progress-bar {
box-sizing: border-box;
position: relative;
z-index: 0;
width: 100%;
height: 5px;
background-color: $rhap_bar-color;
border-radius: 2px;
}
.rhap_progress-filled {
height: 100%;
position: absolute;
z-index: 2;
background-color: $rhap_theme-color;
border-radius: 2px;
}
.rhap_progress-bar-show-download {
background-color: rgba($rhap_bar-color, 0.5);
}
.rhap_download-progress {
height: 100%;
position: absolute;
z-index: 1;
background-color: $rhap_bar-color;
border-radius: 2px;
}
.rhap_progress-indicator {
box-sizing: border-box;
position: absolute;
z-index: 3;
width: 20px;
height: 20px;
margin-left: -10px;
top: -8px;
background: $session-color-green;
border-radius: 50px;
box-shadow: rgba($rhap_theme-color, .5) 0 0 5px;
}
.rhap_controls-section {
display: flex;
justify-content: space-between;
align-items: center;
}
.rhap_additional-controls {
// display: flex;
display: none;
flex: 1 0 auto;
align-items: center;
}
.rhap_repeat-button {
font-size: 26px;
width: 26px;
height: 26px;
color: $rhap_theme-color;
margin-right: 6px;
}
.rhap_main-controls {
flex: 0 1 auto;
display: flex;
align-items: center;
}
.rhap_main-controls-button {
margin: 0 3px;
color: $rhap_theme-color;
font-size: 35px;
width: 25px;
display: flex;
justify-content: start;
}
.rhap_volume-controls {
display: flex;
flex: 1 0 auto;
align-items: center;
}
.rhap_volume-button {
display: flex;
align-items: center;
justify-content: center;
}
.rhap_volume-button {
flex: 0 0 26px;
font-size: 20px;
color: #FFFFFF;
}
.rhap_volume-container {
display: flex;
align-items: center;
flex: 0 1 100px;
-webkit-user-select: none;
}
.rhap_volume-bar-area {
display: flex;
align-items: center;
width: 100%;
height: 14px;
cursor: pointer;
&:focus:not(:focus-visible) {
outline: 0;
}
}
.rhap_volume-bar {
box-sizing: border-box;
position: relative;
width: 100%;
height: 4px;
background: $rhap_bar-color;
border-radius: 2px;
}
.rhap_volume-indicator {
box-sizing: border-box;
position: absolute;
width: 12px;
height: 12px;
margin-left: -6px;
left: 0;
top: -4px;
background: $rhap_theme-color;
opacity: 0.9;
border-radius: 50px;
box-shadow: rgba($rhap_theme-color, .5) 0 0 3px;
cursor: pointer;
&:hover {
opacity: .9;
}
}
/* Utils */
.rhap_button-clear {
background-color: transparent;
border: none;
padding: 0;
overflow: hidden;
cursor: pointer;
&:hover {
opacity: .9;
transition-duration: .2s;
}
&:active {
opacity: .95;
}
&:focus:not(:focus-visible) {
outline: 0;
}
}
/* **************** */
/* END AUDIO PLAYER */
/* **************** */

@ -1646,6 +1646,7 @@ body.dark-theme {
.react-contextmenu-item.react-contextmenu-submenu
> .react-contextmenu-item:after {
content: "";
color: $color-dark-05;
}

@ -64,6 +64,7 @@ interface Props {
selectedMessages: Array<string>;
isKickedFromGroup: boolean;
onInviteContacts: () => void;
onSetDisappearingMessages: (seconds: number) => void;
onDeleteMessages: () => void;
onDeleteContact: () => void;

@ -14,7 +14,7 @@ import { EmbeddedContact } from './EmbeddedContact';
// Audio Player
import H5AudioPlayer from 'react-h5-audio-player';
import 'react-h5-audio-player/lib/styles.css';
// import 'react-h5-audio-player/lib/styles.css';
import {
canDisplayImage,
@ -431,8 +431,14 @@ export class Message extends React.PureComponent<Props, State> {
</audio> */}
<H5AudioPlayer
src={firstAttachment.url}
layout="horizontal"
layout="horizontal-reverse"
showSkipControls={false}
showJumpControls={false}
showDownloadProgress={false}
customIcons={{
play: <SessionIcon iconType={SessionIconType.Play} iconSize={SessionIconSize.Small}/>,
pause: <SessionIcon iconType={SessionIconType.Pause} iconSize={SessionIconSize.Small}/>,
}}
/>
</div>
);
@ -1133,7 +1139,7 @@ export class Message extends React.PureComponent<Props, State> {
const isIncoming = direction === 'incoming';
const shouldHightlight = mentionMe && isIncoming && isPublic;
const divClasses = ['loki-message-wrapper'];
const divClasses = ['session-message-wrapper'];
if (shouldHightlight) {
//divClasses.push('message-highlighted');

@ -10,6 +10,7 @@ import { SessionDropdown } from './SessionDropdown';
import { MediaGallery } from '../conversation/media-gallery/MediaGallery';
import _ from 'lodash';
import { TimerOption } from '../conversation/ConversationHeader';
import { Constants } from '../../session';
interface Props {
id: string;
@ -21,7 +22,7 @@ interface Props {
isPublic: boolean;
onGoBack: () => void;
onInviteFriends: () => void;
onInviteContacts: () => void;
onLeaveGroup: () => void;
onShowLightBox: (options: any) => void;
onSetDisappearingMessages: (seconds: number) => void;
@ -69,20 +70,18 @@ export class SessionChannelSettings extends React.Component<Props, any> {
public async getMediaGalleryProps() {
// We fetch more documents than media as they dont require to be loaded
// into memory right away. Revisit this once we have infinite scrolling:
const DEFAULT_MEDIA_FETCH_COUNT = 50;
const DEFAULT_DOCUMENTS_FETCH_COUNT = 150;
const conversationId = this.props.id;
const rawMedia = await window.Signal.Data.getMessagesWithVisualMediaAttachments(
conversationId,
{
limit: DEFAULT_MEDIA_FETCH_COUNT,
limit: Constants.CONVERSATION.DEFAULT_MEDIA_FETCH_COUNT,
MessageCollection: window.Whisper.MessageCollection,
}
);
const rawDocuments = await window.Signal.Data.getMessagesWithFileAttachments(
conversationId,
{
limit: DEFAULT_DOCUMENTS_FETCH_COUNT,
limit: Constants.CONVERSATION.DEFAULT_DOCUMENTS_FETCH_COUNT,
MessageCollection: window.Whisper.MessageCollection,
}
);
@ -269,7 +268,7 @@ export class SessionChannelSettings extends React.Component<Props, any> {
}
private renderHeader() {
const { id, onGoBack, onInviteFriends, avatarPath } = this.props;
const { id, onGoBack, onInviteContacts, avatarPath } = this.props;
const shouldShowInviteFriends = !this.props.isPublic;
return (
@ -292,7 +291,7 @@ export class SessionChannelSettings extends React.Component<Props, any> {
<SessionIconButton
iconType={SessionIconType.AddUser}
iconSize={SessionIconSize.Medium}
onClick={onInviteFriends}
onClick={onInviteContacts}
/>
)}
</div>

@ -1,364 +0,0 @@
import React from 'react';
import { SessionIconButton, SessionIconSize, SessionIconType } from './icon';
import { Avatar } from '../Avatar';
import {
SessionButton,
SessionButtonColor,
SessionButtonType,
} from './SessionButton';
import { SessionDropdown } from './SessionDropdown';
import { MediaGallery } from '../conversation/media-gallery/MediaGallery';
import _ from 'lodash';
import { TimerOption } from '../conversation/ConversationHeader';
interface Props {
id: string;
name: string;
memberCount: number;
description: string;
avatarPath: string;
timerOptions: Array<TimerOption>;
isPublic: boolean;
isAdmin: boolean;
amMod: boolean;
isKickedFromGroup: boolean;
isBlocked: boolean;
onGoBack: () => void;
onInviteFriends: () => void;
onLeaveGroup: () => void;
onUpdateGroupName: () => void;
onUpdateGroupMembers: () => void;
onShowLightBox: (options: any) => void;
onSetDisappearingMessages: (seconds: number) => void;
}
export class SessionGroupSettings extends React.Component<Props, any> {
public constructor(props: Props) {
super(props);
this.state = {
documents: Array<any>(),
media: Array<any>(),
onItemClick: undefined,
};
}
public componentWillMount() {
this.getMediaGalleryProps()
.then(({ documents, media, onItemClick }) => {
this.setState({
documents,
media,
onItemClick,
});
})
.ignore();
}
public componentDidUpdate() {
const mediaScanInterval = 1000;
setTimeout(() => {
this.getMediaGalleryProps()
.then(({ documents, media, onItemClick }) => {
this.setState({
documents,
media,
onItemClick,
});
})
.ignore();
}, mediaScanInterval);
}
public async getMediaGalleryProps() {
// We fetch more documents than media as they dont require to be loaded
// into memory right away. Revisit this once we have infinite scrolling:
const DEFAULT_MEDIA_FETCH_COUNT = 50;
const DEFAULT_DOCUMENTS_FETCH_COUNT = 150;
const conversationId = this.props.id;
const rawMedia = await window.Signal.Data.getMessagesWithVisualMediaAttachments(
conversationId,
{
limit: DEFAULT_MEDIA_FETCH_COUNT,
MessageCollection: window.Whisper.MessageCollection,
}
);
const rawDocuments = await window.Signal.Data.getMessagesWithFileAttachments(
conversationId,
{
limit: DEFAULT_DOCUMENTS_FETCH_COUNT,
MessageCollection: window.Whisper.MessageCollection,
}
);
// First we upgrade these messages to ensure that they have thumbnails
const max = rawMedia.length;
for (let i = 0; i < max; i += 1) {
const message = rawMedia[i];
const { schemaVersion } = message;
if (schemaVersion < message.VERSION_NEEDED_FOR_DISPLAY) {
// Yep, we really do want to wait for each of these
// eslint-disable-next-line no-await-in-loop
rawMedia[i] = await window.Signal.Migrations.upgradeMessageSchema(
message
);
// eslint-disable-next-line no-await-in-loop
await window.Signal.Data.saveMessage(rawMedia[i], {
Message: window.Whisper.Message,
});
}
}
// tslint:disable-next-line: underscore-consistent-invocation
const media = _.flatten(
rawMedia.map((message: { attachments: any }) => {
const { attachments } = message;
return (attachments || [])
.filter(
(attachment: { thumbnail: any; pending: any; error: any }) =>
attachment.thumbnail && !attachment.pending && !attachment.error
)
.map(
(
attachment: { path?: any; contentType?: any; thumbnail?: any },
index: any
) => {
const { thumbnail } = attachment;
return {
objectURL: window.Signal.Migrations.getAbsoluteAttachmentPath(
attachment.path
),
thumbnailObjectUrl: thumbnail
? window.Signal.Migrations.getAbsoluteAttachmentPath(
thumbnail.path
)
: null,
contentType: attachment.contentType,
index,
attachment,
message,
};
}
);
})
);
// Unlike visual media, only one non-image attachment is supported
const documents = rawDocuments.map(
(message: { attachments: Array<any> }) => {
const attachments = message.attachments || [];
const attachment = attachments[0];
return {
contentType: attachment.contentType,
index: 0,
attachment,
message,
};
}
);
const saveAttachment = async ({ attachment, message }: any = {}) => {
const timestamp = message.received_at;
window.Signal.Types.Attachment.save({
attachment,
document,
getAbsolutePath: window.Signal.Migrations.getAbsoluteAttachmentPath,
timestamp,
});
};
const onItemClick = async ({ message, attachment, type }: any) => {
switch (type) {
case 'documents': {
saveAttachment({ message, attachment }).ignore();
break;
}
case 'media': {
const lightBoxOptions = {
media,
attachment,
message,
};
this.onShowLightBox(lightBoxOptions);
break;
}
default:
throw new TypeError(`Unknown attachment type: '${type}'`);
}
};
return {
media,
documents,
onItemClick,
};
}
public onShowLightBox(options: any) {
this.props.onShowLightBox(options);
}
public render() {
const {
memberCount,
name,
timerOptions,
onLeaveGroup,
isPublic,
isAdmin,
amMod,
isBlocked,
} = this.props;
const { documents, media, onItemClick } = this.state;
const showMemberCount = !!(memberCount && memberCount > 0);
<<<<<<< HEAD
const hasDisappearingMessages = !isPublic;
=======
const hasDisappearingMessages =
!isPublic && !isKickedFromGroup && !isBlocked;
>>>>>>> 5ec3a5b3f7bd86d920f243f5850eecaddedb0da1
const leaveGroupString = isPublic
? window.i18n('leaveOpenGroup')
: window.i18n('leaveClosedGroup');
const disappearingMessagesOptions = timerOptions.map(option => {
return {
content: option.name,
onClick: () => {
this.props.onSetDisappearingMessages(option.value);
},
};
});
<<<<<<< HEAD
const showUpdateGroupNameButton = isPublic ? amMod : isAdmin;
const showUpdateGroupMembersButton = !isPublic && isAdmin;
=======
const showUpdateGroupNameButton =
isPublic && !isKickedFromGroup
? amMod && !isBlocked
: isAdmin && !isBlocked;
const showUpdateGroupMembersButton =
!isPublic && !isKickedFromGroup && !isBlocked && isAdmin;
>>>>>>> 5ec3a5b3f7bd86d920f243f5850eecaddedb0da1
return (
<div className="group-settings">
{this.renderHeader()}
<h2>{name}</h2>
{showMemberCount && (
<>
<div className="spacer-lg" />
<div role="button" className="subtle">
{window.i18n('members', memberCount)}
</div>
<div className="spacer-lg" />
</>
)}
<input
className="description"
placeholder={window.i18n('description')}
/>
{showUpdateGroupNameButton && (
<div
className="group-settings-item"
role="button"
onClick={this.props.onUpdateGroupName}
>
{isPublic
? window.i18n('editGroupNameOrPicture')
: window.i18n('editGroupName')}
</div>
)}
{showUpdateGroupMembersButton && (
<div
className="group-settings-item"
role="button"
onClick={this.props.onUpdateGroupMembers}
>
{window.i18n('showMembers')}
</div>
)}
{/*<div className="group-settings-item">
{window.i18n('notifications')}
</div>
*/}
{hasDisappearingMessages && (
<SessionDropdown
label={window.i18n('disappearingMessages')}
options={disappearingMessagesOptions}
/>
)}
<MediaGallery
documents={documents}
media={media}
onItemClick={onItemClick}
/>
<SessionButton
text={leaveGroupString}
buttonColor={SessionButtonColor.Danger}
buttonType={SessionButtonType.SquareOutline}
onClick={onLeaveGroup}
/>
</div>
);
}
private renderHeader() {
const {
id,
onGoBack,
onInviteFriends,
avatarPath,
isAdmin,
isPublic,
<<<<<<< HEAD
} = this.props;
const showInviteFriends = isPublic || isAdmin;
=======
isKickedFromGroup,
isBlocked,
} = this.props;
const showInviteContacts =
(isPublic || isAdmin) && !isKickedFromGroup && !isBlocked;
>>>>>>> 5ec3a5b3f7bd86d920f243f5850eecaddedb0da1
return (
<div className="group-settings-header">
<SessionIconButton
iconType={SessionIconType.Chevron}
iconSize={SessionIconSize.Medium}
iconRotation={270}
onClick={onGoBack}
/>
<Avatar
avatarPath={avatarPath}
phoneNumber={id}
conversationType="group"
size={80}
/>
<div className="invite-friends-container">
{showInviteFriends && (
<SessionIconButton
iconType={SessionIconType.AddUser}
iconSize={SessionIconSize.Medium}
onClick={onInviteFriends}
/>
)}
</div>
</div>
);
}
}

@ -12,6 +12,8 @@ import { SessionRecording } from './SessionRecording';
import { SignalService } from '../../../../ts/protobuf';
import { Constants } from '../../../session';
interface Props {
placeholder?: string;
@ -156,6 +158,7 @@ export class SessionCompositionBox extends React.Component<Props, State> {
<input
className="hidden"
placeholder="Attachment"
multiple={true}
ref={this.fileInput}
type="file"
@ -174,7 +177,7 @@ export class SessionCompositionBox extends React.Component<Props, State> {
maxRows={3}
ref={this.textarea}
placeholder={placeholder}
maxLength={window.CONSTANTS.MAX_MESSAGE_BODY_LENGTH}
maxLength={Constants.CONVERSATION.MAX_MESSAGE_BODY_LENGTH}
onKeyDown={this.onKeyDown}
value={message}
onChange={this.onChange}

@ -15,6 +15,8 @@ import { getTimestamp } from './SessionConversationManager';
import { SessionScrollButton } from '../SessionScrollButton';
import { SessionGroupSettings } from './SessionGroupSettings';
import { ResetSessionNotification } from '../../conversation/ResetSessionNotification';
import { Constants, getMessageQueue } from '../../../session';
import { MessageQueue } from '../../../session/sending';
interface State {
conversationKey: string;
@ -166,6 +168,7 @@ export class SessionConversation extends React.Component<any, State> {
);
const isRss = conversation.isRss;
// TODO VINCE: OPTIMISE FOR NEW SENDING???
const sendMessageFn = conversationModel.sendMessage.bind(conversationModel);
const shouldRenderGroupSettings =
@ -324,6 +327,7 @@ export class SessionConversation extends React.Component<any, State> {
isOnline={headerProps.isOnline}
selectedMessages={headerProps.selectedMessages}
isKickedFromGroup={headerProps.isKickedFromGroup}
onInviteContacts={headerProps.onInviteContacts}
onSetDisappearingMessages={headerProps.onSetDisappearingMessages}
onDeleteMessages={headerProps.onDeleteMessages}
onDeleteContact={headerProps.onDeleteContact}
@ -377,7 +381,7 @@ export class SessionConversation extends React.Component<any, State> {
public async getMessages(
numMessages?: number,
fetchInterval = window.CONSTANTS.MESSAGE_FETCH_INTERVAL
fetchInterval = Constants.CONVERSATION.MESSAGE_FETCH_INTERVAL
) {
const { conversationKey, messageFetchTimestamp } = this.state;
const timestamp = getTimestamp();
@ -391,11 +395,11 @@ export class SessionConversation extends React.Component<any, State> {
let msgCount =
numMessages ||
Number(window.CONSTANTS.DEFAULT_MESSAGE_FETCH_COUNT) +
Number(Constants.CONVERSATION.DEFAULT_MESSAGE_FETCH_COUNT) +
this.state.unreadCount;
msgCount =
msgCount > window.CONSTANTS.MAX_MESSAGE_FETCH_COUNT
? window.CONSTANTS.MAX_MESSAGE_FETCH_COUNT
msgCount > Constants.CONVERSATION.MAX_MESSAGE_FETCH_COUNT
? Constants.CONVERSATION.MAX_MESSAGE_FETCH_COUNT
: msgCount;
const messageSet = await window.Signal.Data.getMessagesByConversation(
@ -527,6 +531,10 @@ export class SessionConversation extends React.Component<any, State> {
onLeaveGroup: () => {
window.Whisper.events.trigger('leaveGroup', conversation);
},
onInviteContacts: () => {
// VINCE TODO: Inviting contacts ⚡️
return;
},
onAddModerators: () => {
window.Whisper.events.trigger('addModerators', conversation);
@ -567,12 +575,15 @@ export class SessionConversation extends React.Component<any, State> {
phoneNumber: conversation.getNumber(),
profileName: conversation.getProfileName(),
color: conversation.getColor(),
description: '', // TODO VINCE: ENSURE DESCRIPTION IS SET
avatarPath: conversation.getAvatarPath(),
isKickedFromGroup: conversation.isKickedFromGroup(),
amMod: conversation.isModerator(),
isKickedFromGroup: conversation.attributes.isKickedFromGroup,
isGroup: !conversation.isPrivate(),
isPublic: conversation.isPublic(),
isAdmin: conversation.get('groupAdmins').includes(ourPK),
isRss: conversation.isRss(),
isBlocked: conversation.isBlocked(),
timerOptions: window.Whisper.ExpirationTimerOptions.map((item: any) => ({
name: item.getName(),
@ -593,7 +604,8 @@ export class SessionConversation extends React.Component<any, State> {
window.Whisper.events.trigger('updateGroupMembers', conversation);
},
onInviteContacts: () => {
// VINCE TODO: Inviting contacts
// VINCE TODO: Inviting contacts ⚡️
return;
},
onLeaveGroup: () => {
window.Whisper.events.trigger('leaveGroup', conversation);
@ -602,8 +614,6 @@ export class SessionConversation extends React.Component<any, State> {
onShowLightBox: (lightBoxOptions = {}) => {
conversation.showChannelLightbox(lightBoxOptions);
},
};
}
@ -794,12 +804,12 @@ export class SessionConversation extends React.Component<any, State> {
// Fetch more messages when nearing the top of the message list
const shouldFetchMoreMessages =
scrollTop <= window.CONSTANTS.MESSAGE_CONTAINER_BUFFER_OFFSET_PX;
scrollTop <= Constants.UI.MESSAGE_CONTAINER_BUFFER_OFFSET_PX;
if (shouldFetchMoreMessages) {
const numMessages =
this.state.messages.length +
window.CONSTANTS.DEFAULT_MESSAGE_FETCH_COUNT;
Constants.CONVERSATION.DEFAULT_MESSAGE_FETCH_COUNT;
// Prevent grabbing messags with scroll more frequently than once per 5s.
const messageFetchInterval = 2;

@ -1,3 +1,5 @@
import { Constants } from '../../../session';
export interface MessageFetchType {
messages: Array<any>;
messageFetchTimestamp: number;
@ -12,8 +14,7 @@ export async function getMessages(
unreadCount: number,
onGotMessages?: any,
numMessages?: number,
fetchInterval = window.CONSTANTS.MESSAGE_FETCH_INTERVAL,
loopback = false
fetchInterval = Constants.CONVERSATION.MESSAGE_FETCH_INTERVAL,
) {
const timestamp = getTimestamp();
@ -33,10 +34,10 @@ export async function getMessages(
}
let msgCount =
numMessages || window.CONSTANTS.DEFAULT_MESSAGE_FETCH_COUNT + unreadCount;
numMessages || Constants.CONVERSATION.DEFAULT_MESSAGE_FETCH_COUNT + unreadCount;
msgCount =
msgCount > window.CONSTANTS.MAX_MESSAGE_FETCH_COUNT
? window.CONSTANTS.MAX_MESSAGE_FETCH_COUNT
msgCount > Constants.CONVERSATION.MAX_MESSAGE_FETCH_COUNT
? Constants.CONVERSATION.MAX_MESSAGE_FETCH_COUNT
: msgCount;
const messageSet = await window.Signal.Data.getMessagesByConversation(

@ -1,6 +1,7 @@
import React from 'react';
import classNames from 'classnames';
import 'emoji-mart/css/emoji-mart.css'
import { Picker } from 'emoji-mart';
interface Props {
@ -22,7 +23,7 @@ export class SessionEmojiPanel extends React.Component<Props, State> {
};
}
render() {
public render() {
const { onEmojiClicked, show } = this.props;
return (
@ -31,6 +32,7 @@ export class SessionEmojiPanel extends React.Component<Props, State> {
backgroundImageFn={(_set, sheetSize) =>
`./images/emoji/emoji-sheet-${sheetSize}.png`
}
sheetSize={64}
darkMode={true}
color={'#00F782'}
showPreview={true}

@ -10,18 +10,20 @@ import { SessionDropdown } from '../SessionDropdown';
import { MediaGallery } from '../../conversation/media-gallery/MediaGallery';
import _ from 'lodash';
import { TimerOption } from '../../conversation/ConversationHeader';
import { Constants } from '../../../session';
interface Props {
id: string;
name: string;
memberCount: number;
description?: string;
description: string;
avatarPath: string;
timerOptions: Array<TimerOption>;
isPublic: boolean;
isAdmin?: boolean;
amMod?: boolean;
isAdmin: boolean;
amMod: boolean;
isKickedFromGroup: boolean;
isBlocked: boolean;
onGoBack: () => void;
onInviteContacts: () => void;
@ -78,14 +80,14 @@ export class SessionGroupSettings extends React.Component<Props, any> {
const rawMedia = await window.Signal.Data.getMessagesWithVisualMediaAttachments(
conversationId,
{
limit: window.CONSTANTS.DEFAULT_MEDIA_FETCH_COUNT,
limit: Constants.CONVERSATION.DEFAULT_MEDIA_FETCH_COUNT,
MessageCollection: window.Whisper.MessageCollection,
}
);
const rawDocuments = await window.Signal.Data.getMessagesWithFileAttachments(
conversationId,
{
limit: window.CONSTANTS.DEFAULT_DOCUMENTS_FETCH_COUNT,
limit: Constants.CONVERSATION.DEFAULT_DOCUMENTS_FETCH_COUNT,
MessageCollection: window.Whisper.MessageCollection,
}
);
@ -209,18 +211,18 @@ export class SessionGroupSettings extends React.Component<Props, any> {
name,
timerOptions,
onLeaveGroup,
isKickedFromGroup,
isPublic,
isAdmin,
isKickedFromGroup,
amMod,
isBlocked,
} = this.props;
const { documents, media, onItemClick } = this.state;
const showMemberCount = !!(memberCount && memberCount > 0);
const hasDisappearingMessages = !isPublic && !isKickedFromGroup;
const hasDisappearingMessages =
!isPublic && !isKickedFromGroup && !isBlocked;
const leaveGroupString = isPublic
? window.i18n('leaveOpenGroup')
: isKickedFromGroup
? window.i18n('youGotKickedFromGroup')
: window.i18n('leaveClosedGroup');
const disappearingMessagesOptions = timerOptions.map(option => {
@ -233,9 +235,11 @@ export class SessionGroupSettings extends React.Component<Props, any> {
});
const showUpdateGroupNameButton =
isPublic && !isKickedFromGroup ? amMod : isAdmin;
isPublic && !isKickedFromGroup
? amMod && !isBlocked
: isAdmin && !isBlocked;
const showUpdateGroupMembersButton =
!isPublic && !isKickedFromGroup && isAdmin;
!isPublic && !isKickedFromGroup && !isBlocked && isAdmin;
return (
<div className="group-settings">
@ -296,7 +300,6 @@ export class SessionGroupSettings extends React.Component<Props, any> {
buttonColor={SessionButtonColor.Danger}
buttonType={SessionButtonType.SquareOutline}
onClick={onLeaveGroup}
disabled={isKickedFromGroup}
/>
</div>
);
@ -311,9 +314,11 @@ export class SessionGroupSettings extends React.Component<Props, any> {
isAdmin,
isPublic,
isKickedFromGroup,
isBlocked,
} = this.props;
const showInviteContacts = (isPublic || isAdmin) && !isKickedFromGroup;
const showInviteContacts =
(isPublic || isAdmin) && !isKickedFromGroup && !isBlocked;
return (
<div className="group-settings-header">

@ -10,6 +10,7 @@ import {
SessionButtonType,
SessionButtonColor,
} from '../SessionButton';
import { Constants } from '../../../session';
interface Props {
sendVoiceMessage: any;
@ -288,7 +289,7 @@ export class SessionRecording extends React.Component<Props, State> {
const elapsedTime = nowTimestamp - startTimestamp;
// Prevent voice messages exceeding max length.
if (elapsedTime >= window.CONSTANTS.MAX_VOICE_MESSAGE_DURATION) {
if (elapsedTime >= Constants.CONVERSATION.MAX_VOICE_MESSAGE_DURATION) {
this.stopRecordingStream();
}
@ -424,7 +425,7 @@ export class SessionRecording extends React.Component<Props, State> {
}
// Is the audio file > attachment filesize limit
if (audioBlob.size > window.CONSTANTS.MAX_ATTACHMENT_FILESIZE) {
if (audioBlob.size > Constants.CONVERSATION.MAX_ATTACHMENT_FILESIZE) {
console.log(
`[send] Voice message too large: ${audioBlob.size / 1000000} MB`
);

@ -11,3 +11,25 @@ export const TTL_DEFAULT = {
ONLINE_BROADCAST: NumberUtils.timeAsMs(1, 'minute'),
REGULAR_MESSAGE: NumberUtils.timeAsMs(2, 'days'),
};
// User Interface
export const CONVERSATION = {
MAX_MESSAGE_BODY_LENGTH: 2000,
DEFAULT_MEDIA_FETCH_COUNT: 50,
DEFAULT_DOCUMENTS_FETCH_COUNT: 150,
DEFAULT_MESSAGE_FETCH_COUNT: 30,
MAX_MESSAGE_FETCH_COUNT: 500,
MESSAGE_FETCH_INTERVAL: 1,
// Maximum voice message duraiton of 5 minutes
// which equates to 1.97 MB
MAX_VOICE_MESSAGE_DURATION: 300,
// Max attachment size: 10 MB
MAX_ATTACHMENT_FILESIZE: 10000000,
};
export const UI = {
// Pixels (scroll) from the top of the top of message container
// at which more messages should be loaded
MESSAGE_CONTAINER_BUFFER_OFFSET_PX: 30,
};

@ -0,0 +1,10 @@
module.exports = {
module: {
rules: [
{
test: /\.css$/i,
use: ['style-loader', 'css-loader'],
},
],
},
};

@ -336,6 +336,11 @@
dependencies:
"@types/sizzle" "*"
"@types/json-schema@^7.0.4":
version "7.0.5"
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.5.tgz#dcce4430e64b443ba8945f0290fb564ad5bac6dd"
integrity sha512-7+2BITlgjgDhH0vvwZU/HZJVyk+2XUlvxXe8dFMedNX/aMkaOq++rMAFXc0tM7ij15QaWlbdQASBR9dihi+bDQ==
"@types/linkify-it@2.0.3":
version "2.0.3"
resolved "https://registry.yarnpkg.com/@types/linkify-it/-/linkify-it-2.0.3.tgz#5352a2d7a35d7c77b527483cd6e68da9148bd780"
@ -647,6 +652,11 @@ ajv-keywords@^3.0.0, ajv-keywords@^3.1.0:
resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.4.1.tgz#ef916e271c64ac12171fd8384eaae6b2345854da"
integrity sha512-RO1ibKvd27e6FEShVFfPALuHI3WjSVNeK5FIsmme/LYRNxjKuNj+Dt7bucLa6NdSv3JcVTyMlm9kGR84z1XpaQ==
ajv-keywords@^3.4.1:
version "3.5.1"
resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.1.tgz#b83ca89c5d42d69031f424cad49aada0236c6957"
integrity sha512-KWcq3xN8fDjSB+IMoh2VaXVhRI0BBGxoYp3rx7Pkb6z0cFjYR9Q9l4yZqqals0/zsioCmocC5H6UvsGD4MoIBA==
ajv@^5.1.0, ajv@^5.3.0:
version "5.5.2"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.5.2.tgz#73b5eeca3fab653e3d3f9422b341ad42205dc965"
@ -667,6 +677,16 @@ ajv@^6.0.1, ajv@^6.1.0, ajv@^6.5.5:
json-schema-traverse "^0.4.1"
uri-js "^4.2.2"
ajv@^6.12.2:
version "6.12.3"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.3.tgz#18c5af38a111ddeb4f2697bd78d68abc1cabd706"
integrity sha512-4K0cK3L1hsqk9xIb2z9vs/XU+PGJZ9PNpJRDS9YLzmNdX6jmVPfamLvTJr0aDAusnHyCHO6MjzlkAsgtqp9teA==
dependencies:
fast-deep-equal "^3.1.1"
fast-json-stable-stringify "^2.0.0"
json-schema-traverse "^0.4.1"
uri-js "^4.2.2"
alphanum-sort@^1.0.1, alphanum-sort@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/alphanum-sort/-/alphanum-sort-1.0.2.tgz#97a1119649b211ad33691d9f9f486a8ec9fbe0a3"
@ -2519,6 +2539,25 @@ css-loader@^0.28.11:
postcss-value-parser "^3.3.0"
source-list-map "^2.0.0"
css-loader@^3.6.0:
version "3.6.0"
resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-3.6.0.tgz#2e4b2c7e6e2d27f8c8f28f61bffcd2e6c91ef645"
integrity sha512-M5lSukoWi1If8dhQAUCvj4H8vUt3vOnwbQBH9DdTm/s4Ym2B/3dPMtYZeJmq7Q3S3Pa+I94DcZ7pc9bP14cWIQ==
dependencies:
camelcase "^5.3.1"
cssesc "^3.0.0"
icss-utils "^4.1.1"
loader-utils "^1.2.3"
normalize-path "^3.0.0"
postcss "^7.0.32"
postcss-modules-extract-imports "^2.0.0"
postcss-modules-local-by-default "^3.0.2"
postcss-modules-scope "^2.2.0"
postcss-modules-values "^3.0.0"
postcss-value-parser "^4.1.0"
schema-utils "^2.7.0"
semver "^6.3.0"
css-parse@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/css-parse/-/css-parse-2.0.0.tgz#a468ee667c16d81ccf05c58c38d2a97c780dbfd4"
@ -4936,6 +4975,13 @@ icss-utils@^2.1.0:
dependencies:
postcss "^6.0.1"
icss-utils@^4.0.0, icss-utils@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-4.1.1.tgz#21170b53789ee27447c2f47dd683081403f9a467"
integrity sha512-4aFq7wvWyMHKgxsH8QQtGpvbASCf+eM3wPRLI6R+MgAnTCZ6STYsRvttLvRWK0Nfif5piF394St3HeJDaljGPA==
dependencies:
postcss "^7.0.14"
ieee754@^1.1.4:
version "1.1.13"
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84"
@ -5979,7 +6025,7 @@ loader-runner@^2.3.0:
resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.4.0.tgz#ed47066bfe534d7e84c4c7b9998c2a75607d9357"
integrity sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw==
loader-utils@^1.0.2, loader-utils@^1.1.0:
loader-utils@^1.0.2, loader-utils@^1.1.0, loader-utils@^1.2.3:
version "1.4.0"
resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.4.0.tgz#c579b5e34cb34b1a74edc6c1fb36bfa371d5a613"
integrity sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==
@ -7703,6 +7749,13 @@ postcss-modules-extract-imports@^1.2.0:
dependencies:
postcss "^6.0.1"
postcss-modules-extract-imports@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-2.0.0.tgz#818719a1ae1da325f9832446b01136eeb493cd7e"
integrity sha512-LaYLDNS4SG8Q5WAWqIJgdHPJrDDr/Lv775rMBFUbgjTz6j34lUznACHcdRWroPvXANP2Vj7yNK57vp9eFqzLWQ==
dependencies:
postcss "^7.0.5"
postcss-modules-local-by-default@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-1.2.0.tgz#f7d80c398c5a393fa7964466bd19500a7d61c069"
@ -7711,6 +7764,16 @@ postcss-modules-local-by-default@^1.2.0:
css-selector-tokenizer "^0.7.0"
postcss "^6.0.1"
postcss-modules-local-by-default@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-3.0.2.tgz#e8a6561be914aaf3c052876377524ca90dbb7915"
integrity sha512-jM/V8eqM4oJ/22j0gx4jrp63GSvDH6v86OqyTHHUvk4/k1vceipZsaymiZ5PvocqZOl5SFHiFJqjs3la0wnfIQ==
dependencies:
icss-utils "^4.1.1"
postcss "^7.0.16"
postcss-selector-parser "^6.0.2"
postcss-value-parser "^4.0.0"
postcss-modules-scope@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-1.1.0.tgz#d6ea64994c79f97b62a72b426fbe6056a194bb90"
@ -7719,6 +7782,14 @@ postcss-modules-scope@^1.1.0:
css-selector-tokenizer "^0.7.0"
postcss "^6.0.1"
postcss-modules-scope@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-2.2.0.tgz#385cae013cc7743f5a7d7602d1073a89eaae62ee"
integrity sha512-YyEgsTMRpNd+HmyC7H/mh3y+MeFWevy7V1evVhJWewmMbjDHIbZbOXICC2y+m1xI1UVfIT1HMW/O04Hxyu9oXQ==
dependencies:
postcss "^7.0.6"
postcss-selector-parser "^6.0.0"
postcss-modules-values@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-1.3.0.tgz#ecffa9d7e192518389f42ad0e83f72aec456ea20"
@ -7727,6 +7798,14 @@ postcss-modules-values@^1.3.0:
icss-replace-symbols "^1.1.0"
postcss "^6.0.1"
postcss-modules-values@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-3.0.0.tgz#5b5000d6ebae29b4255301b4a3a54574423e7f10"
integrity sha512-1//E5jCBrZ9DmRX+zCtmQtRSV6PV42Ix7Bzj9GbwJceduuf7IqP8MgeTXuRDHOWj2m0VzZD5+roFWDuU8RQjcg==
dependencies:
icss-utils "^4.0.0"
postcss "^7.0.6"
postcss-normalize-charset@^1.1.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/postcss-normalize-charset/-/postcss-normalize-charset-1.1.1.tgz#ef9ee71212d7fe759c78ed162f61ed62b5cb93f1"
@ -7785,6 +7864,15 @@ postcss-selector-parser@^2.0.0, postcss-selector-parser@^2.2.2:
indexes-of "^1.0.1"
uniq "^1.0.1"
postcss-selector-parser@^6.0.0, postcss-selector-parser@^6.0.2:
version "6.0.2"
resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.2.tgz#934cf799d016c83411859e09dcecade01286ec5c"
integrity sha512-36P2QR59jDTOAiIkqEprfJDsoNrvwFei3eCqKd1Y0tUsBimsq39BLp7RD+JWny3WgB1zGhJX8XVePwm9k4wdBg==
dependencies:
cssesc "^3.0.0"
indexes-of "^1.0.1"
uniq "^1.0.1"
postcss-svgo@^2.1.1:
version "2.1.6"
resolved "https://registry.yarnpkg.com/postcss-svgo/-/postcss-svgo-2.1.6.tgz#b6df18aa613b666e133f08adb5219c2684ac108d"
@ -7809,6 +7897,11 @@ postcss-value-parser@^3.0.1, postcss-value-parser@^3.0.2, postcss-value-parser@^
resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz#9ff822547e2893213cf1c30efa51ac5fd1ba8281"
integrity sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==
postcss-value-parser@^4.0.0, postcss-value-parser@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz#443f6a20ced6481a2bda4fa8532a6e55d789a2cb"
integrity sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==
postcss-zindex@^2.0.1:
version "2.2.0"
resolved "https://registry.yarnpkg.com/postcss-zindex/-/postcss-zindex-2.2.0.tgz#d2109ddc055b91af67fc4cb3b025946639d2af22"
@ -7837,6 +7930,15 @@ postcss@^6.0.1:
source-map "^0.6.1"
supports-color "^5.4.0"
postcss@^7.0.14, postcss@^7.0.16, postcss@^7.0.32, postcss@^7.0.5, postcss@^7.0.6:
version "7.0.32"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.32.tgz#4310d6ee347053da3433db2be492883d62cec59d"
integrity sha512-03eXong5NLnNCD05xscnGKGDZ98CyzoqPSMjOe6SuoQY7Z2hIj0Ld1g/O/UQRuOle2aRtiIRDg9tDcTGAkLfKw==
dependencies:
chalk "^2.4.2"
source-map "^0.6.1"
supports-color "^6.1.0"
prelude-ls@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
@ -9139,6 +9241,15 @@ schema-utils@^0.4.2, schema-utils@^0.4.5:
ajv "^6.1.0"
ajv-keywords "^3.1.0"
schema-utils@^2.7.0:
version "2.7.0"
resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.0.tgz#17151f76d8eae67fbbf77960c33c676ad9f4efc7"
integrity sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A==
dependencies:
"@types/json-schema" "^7.0.4"
ajv "^6.12.2"
ajv-keywords "^3.4.1"
scss-tokenizer@^0.2.3:
version "0.2.3"
resolved "https://registry.yarnpkg.com/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz#8eb06db9a9723333824d3f5530641149847ce5d1"
@ -9898,6 +10009,13 @@ supports-color@^5.1.0, supports-color@^5.3.0, supports-color@^5.4.0:
dependencies:
has-flag "^3.0.0"
supports-color@^6.1.0:
version "6.1.0"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-6.1.0.tgz#0764abc69c63d5ac842dd4867e8d025e880df8f3"
integrity sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==
dependencies:
has-flag "^3.0.0"
supports-color@^7.1.0:
version "7.1.0"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.1.0.tgz#68e32591df73e25ad1c4b49108a2ec507962bfd1"

Loading…
Cancel
Save