Display emoji and sticker in room settings
This commit is contained in:
parent
d678b05259
commit
4fd2d3adc2
12 changed files with 316 additions and 16 deletions
|
@ -26,10 +26,10 @@
|
|||
&--icon {
|
||||
@include dir.side(padding, var(--sp-tight), var(--sp-loose));
|
||||
|
||||
.ic-raw {
|
||||
@include dir.side(margin, 0, var(--sp-extra-tight));
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
.ic-raw {
|
||||
@include dir.side(margin, 0, var(--sp-extra-tight));
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
85
src/app/molecules/image-pack/ImagePack.jsx
Normal file
85
src/app/molecules/image-pack/ImagePack.jsx
Normal file
|
@ -0,0 +1,85 @@
|
|||
import React, { useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './ImagePack.scss';
|
||||
|
||||
import initMatrix from '../../../client/initMatrix';
|
||||
|
||||
import Button from '../../atoms/button/Button';
|
||||
import Text from '../../atoms/text/Text';
|
||||
import { MenuHeader } from '../../atoms/context-menu/ContextMenu';
|
||||
import ImagePackProfile from './ImagePackProfile';
|
||||
import ImagePackItem from './ImagePackItem';
|
||||
import { ImagePack as ImagePackBuilder, getUserImagePack } from '../../organisms/emoji-board/custom-emoji';
|
||||
|
||||
function getUsage(usage) {
|
||||
if (usage.includes('emoticon') && usage.includes('sticker')) return 'both';
|
||||
if (usage.includes('emoticon')) return 'emoticon';
|
||||
if (usage.includes('sticker')) return 'sticker';
|
||||
|
||||
return 'both';
|
||||
}
|
||||
|
||||
function ImagePack({ roomId, stateKey }) {
|
||||
const mx = initMatrix.matrixClient;
|
||||
const room = mx.getRoom(roomId);
|
||||
const [viewMore, setViewMore] = useState(false);
|
||||
|
||||
const packEvent = roomId
|
||||
? room.currentState.getStateEvents('im.ponies.room_emotes', stateKey)
|
||||
: mx.getAccountData('im.ponies.user_emotes');
|
||||
const pack = roomId
|
||||
? ImagePackBuilder.parsePack(packEvent.getId(), packEvent.getContent(), room)
|
||||
: getUserImagePack(mx);
|
||||
|
||||
return (
|
||||
<div className="image-pack">
|
||||
<MenuHeader>{pack.displayName}</MenuHeader>
|
||||
<ImagePackProfile
|
||||
avatarUrl={mx.mxcUrlToHttp(pack.avatarUrl ?? pack.getEmojis()[0].mxc)}
|
||||
displayName={pack.displayName}
|
||||
attribution={pack.attribution}
|
||||
usage={getUsage(pack.usage)}
|
||||
onUsage={() => false}
|
||||
onEdit={() => false}
|
||||
/>
|
||||
<div className="image-pack__header">
|
||||
<Text variant="b3">Image</Text>
|
||||
<Text variant="b3">Shortcode</Text>
|
||||
<Text variant="b3">Usage</Text>
|
||||
</div>
|
||||
<div>
|
||||
{([...pack.images].slice(0, viewMore ? pack.images.size : 2)).map(([shortcode, image]) => (
|
||||
<ImagePackItem
|
||||
key={shortcode}
|
||||
url={mx.mxcUrlToHttp(image.mxc)}
|
||||
shortcode={shortcode}
|
||||
usage={getUsage(image.usage)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="image-pack__footer">
|
||||
{pack.images.size > 2 && (
|
||||
<Button onClick={() => setViewMore(!viewMore)}>
|
||||
{
|
||||
viewMore
|
||||
? 'View less'
|
||||
: `View ${pack.images.size - 2} more`
|
||||
}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ImagePack.defaultProps = {
|
||||
roomId: null,
|
||||
stateKey: null,
|
||||
};
|
||||
|
||||
ImagePack.propTypes = {
|
||||
roomId: PropTypes.string,
|
||||
stateKey: PropTypes.string,
|
||||
};
|
||||
|
||||
export default ImagePack;
|
23
src/app/molecules/image-pack/ImagePack.scss
Normal file
23
src/app/molecules/image-pack/ImagePack.scss
Normal file
|
@ -0,0 +1,23 @@
|
|||
@use '../../partials/flex';
|
||||
|
||||
.image-pack {
|
||||
&-item {
|
||||
border-top: 1px solid var(--bg-surface-border);
|
||||
}
|
||||
|
||||
&__header {
|
||||
margin-top: var(--sp-normal);
|
||||
padding: var(--sp-extra-tight) var(--sp-normal);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-normal);
|
||||
|
||||
& > *:nth-child(2) {
|
||||
@extend .cp-fx__item-one;
|
||||
}
|
||||
}
|
||||
|
||||
&__footer {
|
||||
padding: var(--sp-normal);
|
||||
}
|
||||
}
|
39
src/app/molecules/image-pack/ImagePackItem.jsx
Normal file
39
src/app/molecules/image-pack/ImagePackItem.jsx
Normal file
|
@ -0,0 +1,39 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './ImagePackItem.scss';
|
||||
|
||||
import Avatar from '../../atoms/avatar/Avatar';
|
||||
import Text from '../../atoms/text/Text';
|
||||
import Button from '../../atoms/button/Button';
|
||||
import RawIcon from '../../atoms/system-icons/RawIcon';
|
||||
|
||||
import ChevronBottomIC from '../../../../public/res/ic/outlined/chevron-bottom.svg';
|
||||
|
||||
function ImagePackItem({ url, shortcode, usage }) {
|
||||
return (
|
||||
<div className="image-pack-item">
|
||||
<Avatar imageSrc={url} size="extra-small" text={shortcode} bgColor="black" />
|
||||
<div className="image-pack-item__content">
|
||||
<Text>{shortcode}</Text>
|
||||
</div>
|
||||
<div className="image-pack-item__usage">
|
||||
<Button>
|
||||
<RawIcon src={ChevronBottomIC} size="extra-small" />
|
||||
<Text variant="b2">
|
||||
{usage === 'emoticon' && 'Emoji'}
|
||||
{usage === 'sticker' && 'Sticker'}
|
||||
{usage === 'both' && 'Both'}
|
||||
</Text>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ImagePackItem.propTypes = {
|
||||
url: PropTypes.string.isRequired,
|
||||
shortcode: PropTypes.string.isRequired,
|
||||
usage: PropTypes.oneOf(['emoticon', 'sticker', 'both']).isRequired,
|
||||
};
|
||||
|
||||
export default ImagePackItem;
|
17
src/app/molecules/image-pack/ImagePackItem.scss
Normal file
17
src/app/molecules/image-pack/ImagePackItem.scss
Normal file
|
@ -0,0 +1,17 @@
|
|||
@use '../../partials/flex';
|
||||
|
||||
.image-pack-item {
|
||||
margin: 0 var(--sp-normal);
|
||||
padding: var(--sp-tight) 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-normal);
|
||||
|
||||
&__content {
|
||||
@extend .cp-fx__item-one;
|
||||
}
|
||||
|
||||
&__usage > button {
|
||||
padding: 6px var(--sp-extra-tight);
|
||||
}
|
||||
}
|
53
src/app/molecules/image-pack/ImagePackProfile.jsx
Normal file
53
src/app/molecules/image-pack/ImagePackProfile.jsx
Normal file
|
@ -0,0 +1,53 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './ImagePackProfile.scss';
|
||||
|
||||
import Text from '../../atoms/text/Text';
|
||||
import Avatar from '../../atoms/avatar/Avatar';
|
||||
import Button from '../../atoms/button/Button';
|
||||
import IconButton from '../../atoms/button/IconButton';
|
||||
|
||||
import ChevronBottomIC from '../../../../public/res/ic/outlined/chevron-bottom.svg';
|
||||
import PencilIC from '../../../../public/res/ic/outlined/pencil.svg';
|
||||
|
||||
function ImagePackProfile({
|
||||
avatarUrl, displayName, attribution, usage, onUsage, onEdit,
|
||||
}) {
|
||||
return (
|
||||
<div className="image-pack-profile">
|
||||
<Avatar text={displayName} bgColor="blue" imageSrc={avatarUrl} size="normal" />
|
||||
<div className="image-pack-profile__content">
|
||||
<div>
|
||||
<Text>{displayName}</Text>
|
||||
{onEdit && <IconButton size="extra-small" onClick={onEdit} src={PencilIC} />}
|
||||
</div>
|
||||
{attribution && <Text variant="b3">{attribution}</Text>}
|
||||
</div>
|
||||
<div className="image-pack-profile__usage">
|
||||
<Text variant="b3">Pack usage</Text>
|
||||
<Button iconSrc={onUsage ? ChevronBottomIC : null} onClick={onUsage}>
|
||||
{usage === 'emoticon' && 'Emoji'}
|
||||
{usage === 'sticker' && 'Sticker'}
|
||||
{usage === 'both' && 'Emoji & Sticker'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ImagePackProfile.defaultProps = {
|
||||
avatarUrl: null,
|
||||
attribution: null,
|
||||
onUsage: null,
|
||||
onEdit: null,
|
||||
};
|
||||
ImagePackProfile.propTypes = {
|
||||
avatarUrl: PropTypes.string,
|
||||
displayName: PropTypes.string.isRequired,
|
||||
attribution: PropTypes.string,
|
||||
usage: PropTypes.oneOf(['emoticon', 'sticker', 'both']).isRequired,
|
||||
onUsage: PropTypes.func,
|
||||
onEdit: PropTypes.func,
|
||||
};
|
||||
|
||||
export default ImagePackProfile;
|
22
src/app/molecules/image-pack/ImagePackProfile.scss
Normal file
22
src/app/molecules/image-pack/ImagePackProfile.scss
Normal file
|
@ -0,0 +1,22 @@
|
|||
@use '../../partials/flex';
|
||||
|
||||
.image-pack-profile {
|
||||
padding: var(--sp-normal);
|
||||
display: flex;
|
||||
|
||||
&__content {
|
||||
padding: 0 var(--sp-tight);
|
||||
@extend .cp-fx__item-one;
|
||||
|
||||
& div:first-child {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-extra-tight);
|
||||
}
|
||||
}
|
||||
&__usage {
|
||||
& > *:first-child {
|
||||
margin-bottom: var(--sp-ultra-tight);
|
||||
}
|
||||
}
|
||||
}
|
22
src/app/molecules/image-pack/ImagePackUsageSelector.jsx
Normal file
22
src/app/molecules/image-pack/ImagePackUsageSelector.jsx
Normal file
|
@ -0,0 +1,22 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { MenuHeader, MenuItem } from '../../atoms/context-menu/ContextMenu';
|
||||
|
||||
function ImagePackUsageSelector({ usage, onSelect }) {
|
||||
return (
|
||||
<div>
|
||||
<MenuHeader>Usage</MenuHeader>
|
||||
<MenuItem variant={usage === 'emoticon' ? 'positive' : 'surface'} onClick={() => onSelect('emoticon')}>Emoji</MenuItem>
|
||||
<MenuItem variant={usage === 'sticker' ? 'positive' : 'surface'} onClick={() => onSelect('sticker')}>Sticker</MenuItem>
|
||||
<MenuItem variant={usage === 'both' ? 'positive' : 'surface'} onClick={() => onSelect('both')}>Both</MenuItem>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ImagePackUsageSelector.propTypes = {
|
||||
usage: PropTypes.oneOf(['emoticon', 'sticker', 'both']).isRequired,
|
||||
onSelect: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default ImagePackUsageSelector;
|
|
@ -213,8 +213,6 @@ function EmojiBoard({ onSelect, searchRef }) {
|
|||
for (let i = 0; i < packs.length; i += 1) {
|
||||
packs[i].packIndex = i;
|
||||
}
|
||||
|
||||
console.log(packs)
|
||||
setAvailableEmojis(packs);
|
||||
}
|
||||
};
|
||||
|
@ -294,7 +292,7 @@ function EmojiBoard({ onSelect, searchRef }) {
|
|||
{
|
||||
availableEmojis.map((pack) => {
|
||||
const src = initMatrix.matrixClient
|
||||
.mxcUrlToHttp(pack.avatar ?? pack.getEmojis()[0].mxc);
|
||||
.mxcUrlToHttp(pack.avatarUrl ?? pack.getEmojis()[0].mxc);
|
||||
return (
|
||||
<IconButton
|
||||
onClick={() => openGroup(recentOffset + pack.packIndex)}
|
||||
|
|
|
@ -11,9 +11,10 @@ class ImagePack {
|
|||
const pack = packContent.pack ?? {};
|
||||
|
||||
const displayName = pack.display_name ?? room?.name ?? undefined;
|
||||
const avatar = pack.avatar_url ?? room?.getMxcAvatarUrl() ?? undefined;
|
||||
const avatarUrl = pack.avatar_url ?? room?.getMxcAvatarUrl() ?? undefined;
|
||||
const packUsage = pack.usage ?? ['emoticon', 'sticker'];
|
||||
const { attribution } = pack;
|
||||
const images = new Map();
|
||||
const emoticons = [];
|
||||
const stickers = [];
|
||||
|
||||
|
@ -28,6 +29,7 @@ class ImagePack {
|
|||
shortcode, mxc, body, usage, info,
|
||||
};
|
||||
|
||||
images.set(shortcode, image);
|
||||
if (usage.includes('emoticon')) {
|
||||
emoticons.push(image);
|
||||
}
|
||||
|
@ -38,9 +40,10 @@ class ImagePack {
|
|||
|
||||
return new ImagePack(eventId, {
|
||||
displayName,
|
||||
avatar,
|
||||
avatarUrl,
|
||||
usage: packUsage,
|
||||
attribution,
|
||||
images,
|
||||
emoticons,
|
||||
stickers,
|
||||
});
|
||||
|
@ -48,22 +51,28 @@ class ImagePack {
|
|||
|
||||
constructor(id, {
|
||||
displayName,
|
||||
avatar,
|
||||
avatarUrl,
|
||||
usage,
|
||||
attribution,
|
||||
images,
|
||||
emoticons,
|
||||
stickers,
|
||||
}) {
|
||||
this.id = id;
|
||||
this.displayName = displayName;
|
||||
this.avatar = avatar;
|
||||
this.avatarUrl = avatarUrl;
|
||||
this.usage = usage;
|
||||
this.attribution = attribution;
|
||||
|
||||
this.images = images;
|
||||
this.emoticons = emoticons;
|
||||
this.stickers = stickers;
|
||||
}
|
||||
|
||||
getImages() {
|
||||
return this.images;
|
||||
}
|
||||
|
||||
getEmojis() {
|
||||
return this.emoticons;
|
||||
}
|
||||
|
@ -175,6 +184,8 @@ function getEmojiForCompletion(room) {
|
|||
}
|
||||
|
||||
export {
|
||||
ImagePack,
|
||||
getUserImagePack, getGlobalImagePacks, getRoomImagePacks,
|
||||
getShortcodeToEmoji, getShortcodeToCustomEmoji,
|
||||
getRelevantPacks, getEmojiForCompletion,
|
||||
};
|
||||
|
|
|
@ -25,9 +25,11 @@ import RoomHistoryVisibility from '../../molecules/room-history-visibility/RoomH
|
|||
import RoomEncryption from '../../molecules/room-encryption/RoomEncryption';
|
||||
import RoomPermissions from '../../molecules/room-permissions/RoomPermissions';
|
||||
import RoomMembers from '../../molecules/room-members/RoomMembers';
|
||||
import ImagePack from '../../molecules/image-pack/ImagePack';
|
||||
|
||||
import UserIC from '../../../../public/res/ic/outlined/user.svg';
|
||||
import SettingsIC from '../../../../public/res/ic/outlined/settings.svg';
|
||||
import EmojiIC from '../../../../public/res/ic/outlined/emoji.svg';
|
||||
import SearchIC from '../../../../public/res/ic/outlined/search.svg';
|
||||
import ShieldUserIC from '../../../../public/res/ic/outlined/shield-user.svg';
|
||||
import LockIC from '../../../../public/res/ic/outlined/lock.svg';
|
||||
|
@ -42,6 +44,7 @@ const tabText = {
|
|||
GENERAL: 'General',
|
||||
SEARCH: 'Search',
|
||||
MEMBERS: 'Members',
|
||||
EMOJIS: 'Emojis',
|
||||
PERMISSIONS: 'Permissions',
|
||||
SECURITY: 'Security',
|
||||
};
|
||||
|
@ -58,6 +61,10 @@ const tabItems = [{
|
|||
iconSrc: UserIC,
|
||||
text: tabText.MEMBERS,
|
||||
disabled: false,
|
||||
}, {
|
||||
iconSrc: EmojiIC,
|
||||
text: tabText.EMOJIS,
|
||||
disabled: false,
|
||||
}, {
|
||||
iconSrc: ShieldUserIC,
|
||||
text: tabText.PERMISSIONS,
|
||||
|
@ -121,6 +128,29 @@ GeneralSettings.propTypes = {
|
|||
roomId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
function RoomEmojis({ roomId }) {
|
||||
const mx = initMatrix.matrixClient;
|
||||
const room = mx.getRoom(roomId);
|
||||
|
||||
const packEvents = room.currentState.getStateEvents('im.ponies.room_emotes');
|
||||
const unUsablePacks = [];
|
||||
const usablePacks = packEvents.filter((mEvent) => {
|
||||
if (typeof mEvent.getContent()?.images !== 'object') {
|
||||
unUsablePacks.push(mEvent);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
return usablePacks.map((mEvent) => (
|
||||
<ImagePack
|
||||
key={mEvent.getId()}
|
||||
roomId={roomId}
|
||||
stateKey={mEvent.getStateKey()}
|
||||
/>
|
||||
));
|
||||
}
|
||||
|
||||
function SecuritySettings({ roomId }) {
|
||||
return (
|
||||
<>
|
||||
|
@ -197,6 +227,7 @@ function RoomSettings({ roomId }) {
|
|||
{selectedTab.text === tabText.GENERAL && <GeneralSettings roomId={roomId} />}
|
||||
{selectedTab.text === tabText.SEARCH && <RoomSearch roomId={roomId} />}
|
||||
{selectedTab.text === tabText.MEMBERS && <RoomMembers roomId={roomId} />}
|
||||
{selectedTab.text === tabText.EMOJIS && <RoomEmojis roomId={roomId} />}
|
||||
{selectedTab.text === tabText.PERMISSIONS && <RoomPermissions roomId={roomId} />}
|
||||
{selectedTab.text === tabText.SECURITY && <SecuritySettings roomId={roomId} />}
|
||||
</div>
|
||||
|
@ -210,7 +241,5 @@ RoomSettings.propTypes = {
|
|||
roomId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export {
|
||||
RoomSettings as default,
|
||||
tabText,
|
||||
};
|
||||
export default RoomSettings;
|
||||
export { tabText };
|
||||
|
|
|
@ -76,6 +76,7 @@
|
|||
.room-settings .room-permissions__card,
|
||||
.room-settings .room-search__form,
|
||||
.room-settings .room-search__result-item ,
|
||||
.room-settings .room-members {
|
||||
.room-settings .room-members,
|
||||
.room-settings .image-pack {
|
||||
@extend .room-settings__card;
|
||||
}
|
Loading…
Reference in a new issue