Message popup
This commit is contained in:
@@ -8,7 +8,7 @@ android {
|
|||||||
minSdkVersion rootProject.ext.minSdkVersion
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
versionCode 27
|
versionCode 27
|
||||||
versionName "1.0.29"
|
versionName "1.0.30"
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
aaptOptions {
|
aaptOptions {
|
||||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@discord-clone/electron",
|
"name": "@discord-clone/electron",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.0.29",
|
"version": "1.0.30",
|
||||||
"description": "Discord Clone - Electron app",
|
"description": "Discord Clone - Electron app",
|
||||||
"author": "Moyettes",
|
"author": "Moyettes",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@discord-clone/web",
|
"name": "@discord-clone/web",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.0.29",
|
"version": "1.0.30",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@discord-clone/shared",
|
"name": "@discord-clone/shared",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.0.28",
|
"version": "1.0.30",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/App.jsx",
|
"main": "src/App.jsx",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import Avatar from './Avatar';
|
|||||||
import MentionMenu from './MentionMenu';
|
import MentionMenu from './MentionMenu';
|
||||||
import SlashCommandMenu from './SlashCommandMenu';
|
import SlashCommandMenu from './SlashCommandMenu';
|
||||||
import MessageItem, { getUserColor, parseAttachment } from './MessageItem';
|
import MessageItem, { getUserColor, parseAttachment } from './MessageItem';
|
||||||
|
import MobileMessageDrawer from './MobileMessageDrawer';
|
||||||
import ColoredIcon from './ColoredIcon';
|
import ColoredIcon from './ColoredIcon';
|
||||||
import { usePlatform } from '../platform';
|
import { usePlatform } from '../platform';
|
||||||
import { useVoice } from '../contexts/VoiceContext';
|
import { useVoice } from '../contexts/VoiceContext';
|
||||||
@@ -538,6 +539,7 @@ const ChatArea = ({ channelId, channelName, channelType, username, channelKey, u
|
|||||||
const [ephemeralMessages, setEphemeralMessages] = useState([]);
|
const [ephemeralMessages, setEphemeralMessages] = useState([]);
|
||||||
const [unreadDividerTimestamp, setUnreadDividerTimestamp] = useState(null);
|
const [unreadDividerTimestamp, setUnreadDividerTimestamp] = useState(null);
|
||||||
const [reactionPickerMsgId, setReactionPickerMsgId] = useState(null);
|
const [reactionPickerMsgId, setReactionPickerMsgId] = useState(null);
|
||||||
|
const [mobileDrawer, setMobileDrawer] = useState(null);
|
||||||
|
|
||||||
// Focused mode state (for jumping to old messages not in paginated view)
|
// Focused mode state (for jumping to old messages not in paginated view)
|
||||||
const [focusedMode, setFocusedMode] = useState(false);
|
const [focusedMode, setFocusedMode] = useState(false);
|
||||||
@@ -2049,6 +2051,65 @@ const ChatArea = ({ channelId, channelName, channelType, username, channelKey, u
|
|||||||
setContextMenu(null);
|
setContextMenu(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Long-press handler factory for mobile (not a hook - called per message in render callback)
|
||||||
|
const createLongPressHandlers = (callback) => {
|
||||||
|
let timer = null;
|
||||||
|
let startX = 0;
|
||||||
|
let startY = 0;
|
||||||
|
let triggered = false;
|
||||||
|
return {
|
||||||
|
onTouchStart: (e) => {
|
||||||
|
triggered = false;
|
||||||
|
startX = e.touches[0].clientX;
|
||||||
|
startY = e.touches[0].clientY;
|
||||||
|
timer = setTimeout(() => {
|
||||||
|
triggered = true;
|
||||||
|
if (navigator.vibrate) navigator.vibrate(50);
|
||||||
|
callback();
|
||||||
|
}, 500);
|
||||||
|
},
|
||||||
|
onTouchMove: (e) => {
|
||||||
|
if (!timer) return;
|
||||||
|
const dx = e.touches[0].clientX - startX;
|
||||||
|
const dy = e.touches[0].clientY - startY;
|
||||||
|
if (Math.abs(dx) > 10 || Math.abs(dy) > 10) {
|
||||||
|
clearTimeout(timer);
|
||||||
|
timer = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onTouchEnd: (e) => {
|
||||||
|
if (timer) { clearTimeout(timer); timer = null; }
|
||||||
|
if (triggered) { e.preventDefault(); triggered = false; }
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMobileDrawerAction = (action, messageId) => {
|
||||||
|
const msg = decryptedMessages.find(m => m.id === messageId);
|
||||||
|
if (!msg) return;
|
||||||
|
switch (action) {
|
||||||
|
case 'reply':
|
||||||
|
setReplyingTo({ messageId: msg.id, username: msg.username, content: msg.content?.substring(0, 100) });
|
||||||
|
break;
|
||||||
|
case 'edit':
|
||||||
|
setEditingMessage({ id: msg.id, content: msg.content });
|
||||||
|
setEditInput(msg.content);
|
||||||
|
break;
|
||||||
|
case 'pin':
|
||||||
|
pinMessageMutation({ id: msg.id, pinned: !msg.pinned });
|
||||||
|
break;
|
||||||
|
case 'delete':
|
||||||
|
deleteMessageMutation({ id: msg.id, userId: currentUserId });
|
||||||
|
break;
|
||||||
|
case 'reaction':
|
||||||
|
setReactionPickerMsgId(msg.id);
|
||||||
|
break;
|
||||||
|
case 'copy':
|
||||||
|
if (msg.content) navigator.clipboard.writeText(msg.content).catch(() => {});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const scrollToMessage = useCallback((messageId) => {
|
const scrollToMessage = useCallback((messageId) => {
|
||||||
const idx = decryptedMessages.findIndex(m => m.id === messageId);
|
const idx = decryptedMessages.findIndex(m => m.id === messageId);
|
||||||
if (idx !== -1 && virtuosoRef.current) {
|
if (idx !== -1 && virtuosoRef.current) {
|
||||||
@@ -2244,6 +2305,7 @@ const ChatArea = ({ channelId, channelName, channelType, username, channelKey, u
|
|||||||
onEdit={() => { setEditingMessage({ id: msg.id, content: msg.content }); setEditInput(msg.content); }}
|
onEdit={() => { setEditingMessage({ id: msg.id, content: msg.content }); setEditInput(msg.content); }}
|
||||||
onReply={() => setReplyingTo({ messageId: msg.id, username: msg.username, content: msg.content?.substring(0, 100) })}
|
onReply={() => setReplyingTo({ messageId: msg.id, username: msg.username, content: msg.content?.substring(0, 100) })}
|
||||||
onMore={(e) => { const rect = e.target.getBoundingClientRect(); setContextMenu({ x: rect.left, y: rect.bottom, messageId: msg.id, isOwner, isAttachment: !!parseAttachment(msg.content), canDelete }); }}
|
onMore={(e) => { const rect = e.target.getBoundingClientRect(); setContextMenu({ x: rect.left, y: rect.bottom, messageId: msg.id, isOwner, isAttachment: !!parseAttachment(msg.content), canDelete }); }}
|
||||||
|
onLongPress={isMobile ? createLongPressHandlers(() => setMobileDrawer({ messageId: msg.id, isOwner, isAttachment: !!parseAttachment(msg.content), canDelete, message: msg })) : undefined}
|
||||||
onEditInputChange={(e) => setEditInput(e.target.value)}
|
onEditInputChange={(e) => setEditInput(e.target.value)}
|
||||||
onEditKeyDown={handleEditKeyDown}
|
onEditKeyDown={handleEditKeyDown}
|
||||||
onEditSave={handleEditSave}
|
onEditSave={handleEditSave}
|
||||||
@@ -2258,7 +2320,7 @@ const ChatArea = ({ channelId, channelName, channelType, username, channelKey, u
|
|||||||
DirectVideo={DirectVideo}
|
DirectVideo={DirectVideo}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}, [decryptedMessages, username, myPermissions, isMentionedInContent, unreadDividerTimestamp, editingMessage, hoveredMessageId, editInput, roles, customEmojis, reactionPickerMsgId, currentUserId, addReaction, handleEditKeyDown, handleEditSave, handleReactionClick, scrollToMessage, handleProfilePopup, scrollToBottom]);
|
}, [decryptedMessages, username, myPermissions, isMentionedInContent, unreadDividerTimestamp, editingMessage, hoveredMessageId, editInput, roles, customEmojis, reactionPickerMsgId, currentUserId, isMobile, addReaction, handleEditKeyDown, handleEditSave, handleReactionClick, scrollToMessage, handleProfilePopup, scrollToBottom]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="chat-area" onDragOver={handleDragOver} onDragLeave={handleDragLeave} onDrop={handleDrop} style={{ position: 'relative' }}>
|
<div className="chat-area" onDragOver={handleDragOver} onDragLeave={handleDragLeave} onDrop={handleDrop} style={{ position: 'relative' }}>
|
||||||
@@ -2316,6 +2378,17 @@ const ChatArea = ({ channelId, channelName, channelType, username, channelKey, u
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{contextMenu && <MessageContextMenu x={contextMenu.x} y={contextMenu.y} isOwner={contextMenu.isOwner} isAttachment={contextMenu.isAttachment} canDelete={contextMenu.canDelete} onClose={() => setContextMenu(null)} onInteract={(action) => handleContextInteract(action, contextMenu.messageId)} />}
|
{contextMenu && <MessageContextMenu x={contextMenu.x} y={contextMenu.y} isOwner={contextMenu.isOwner} isAttachment={contextMenu.isAttachment} canDelete={contextMenu.canDelete} onClose={() => setContextMenu(null)} onInteract={(action) => handleContextInteract(action, contextMenu.messageId)} />}
|
||||||
|
{mobileDrawer && (
|
||||||
|
<MobileMessageDrawer
|
||||||
|
message={mobileDrawer.message}
|
||||||
|
isOwner={mobileDrawer.isOwner}
|
||||||
|
isAttachment={mobileDrawer.isAttachment}
|
||||||
|
canDelete={mobileDrawer.canDelete}
|
||||||
|
onClose={() => setMobileDrawer(null)}
|
||||||
|
onAction={(action) => handleMobileDrawerAction(action, mobileDrawer.messageId)}
|
||||||
|
onQuickReaction={(emoji) => { addReaction({ messageId: mobileDrawer.messageId, userId: currentUserId, emoji }); }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{reactionPickerMsgId && (
|
{reactionPickerMsgId && (
|
||||||
<div style={{ position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, zIndex: 999 }} onClick={() => setReactionPickerMsgId(null)}>
|
<div style={{ position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, zIndex: 999 }} onClick={() => setReactionPickerMsgId(null)}>
|
||||||
<div style={{ position: 'absolute', right: '80px', top: '50%', transform: 'translateY(-50%)' }} onClick={(e) => e.stopPropagation()}>
|
<div style={{ position: 'absolute', right: '80px', top: '50%', transform: 'translateY(-50%)' }} onClick={(e) => e.stopPropagation()}>
|
||||||
|
|||||||
@@ -212,6 +212,7 @@ const MessageItem = React.memo(({
|
|||||||
onScrollToMessage,
|
onScrollToMessage,
|
||||||
onProfilePopup,
|
onProfilePopup,
|
||||||
onImageClick,
|
onImageClick,
|
||||||
|
onLongPress,
|
||||||
scrollToBottom,
|
scrollToBottom,
|
||||||
Attachment,
|
Attachment,
|
||||||
LinkPreview,
|
LinkPreview,
|
||||||
@@ -300,6 +301,9 @@ const MessageItem = React.memo(({
|
|||||||
onMouseEnter={onHover}
|
onMouseEnter={onHover}
|
||||||
onMouseLeave={onLeave}
|
onMouseLeave={onLeave}
|
||||||
onContextMenu={onContextMenu}
|
onContextMenu={onContextMenu}
|
||||||
|
onTouchStart={onLongPress?.onTouchStart}
|
||||||
|
onTouchMove={onLongPress?.onTouchMove}
|
||||||
|
onTouchEnd={onLongPress?.onTouchEnd}
|
||||||
>
|
>
|
||||||
{isMentioned && <div style={{ background: 'hsl(34, 50.847%, 53.725%)', bottom: 0, display: 'block', left: 0, pointerEvents: 'none', position: 'absolute', top: 0, width: '2px' }} />}
|
{isMentioned && <div style={{ background: 'hsl(34, 50.847%, 53.725%)', bottom: 0, display: 'block', left: 0, pointerEvents: 'none', position: 'absolute', top: 0, width: '2px' }} />}
|
||||||
|
|
||||||
|
|||||||
158
packages/shared/src/components/MobileMessageDrawer.jsx
Normal file
158
packages/shared/src/components/MobileMessageDrawer.jsx
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
import React, { useState, useRef, useCallback } from 'react';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
import { EmojieIcon, EditIcon, ReplyIcon, DeleteIcon, PinIcon } from '../assets/icons';
|
||||||
|
import { getEmojiUrl } from '../assets/emojis';
|
||||||
|
import ColoredIcon from './ColoredIcon';
|
||||||
|
|
||||||
|
const ICON_COLOR_DEFAULT = 'hsl(240, 4.294%, 68.039%)';
|
||||||
|
|
||||||
|
const QUICK_REACTIONS = [
|
||||||
|
{ name: 'thumbsup', category: 'people' },
|
||||||
|
{ name: 'fire', category: 'nature' },
|
||||||
|
{ name: 'heart', category: 'symbols' },
|
||||||
|
{ name: 'joy', category: 'people' },
|
||||||
|
{ name: 'sob', category: 'people' },
|
||||||
|
{ name: 'eyes', category: 'people' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const CopyIcon = () => (
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
|
||||||
|
<path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
const MobileMessageDrawer = ({ message, isOwner, isAttachment, canDelete, onClose, onAction, onQuickReaction }) => {
|
||||||
|
const [closing, setClosing] = useState(false);
|
||||||
|
const drawerRef = useRef(null);
|
||||||
|
const dragStartY = useRef(null);
|
||||||
|
const dragCurrentY = useRef(null);
|
||||||
|
const dragStartTime = useRef(null);
|
||||||
|
|
||||||
|
const dismiss = useCallback(() => {
|
||||||
|
setClosing(true);
|
||||||
|
setTimeout(onClose, 200);
|
||||||
|
}, [onClose]);
|
||||||
|
|
||||||
|
const handleAction = useCallback((action) => {
|
||||||
|
onAction(action);
|
||||||
|
dismiss();
|
||||||
|
}, [onAction, dismiss]);
|
||||||
|
|
||||||
|
const handleQuickReaction = useCallback((name) => {
|
||||||
|
onQuickReaction(name);
|
||||||
|
dismiss();
|
||||||
|
}, [onQuickReaction, dismiss]);
|
||||||
|
|
||||||
|
// Swipe-to-dismiss
|
||||||
|
const handleTouchStart = useCallback((e) => {
|
||||||
|
dragStartY.current = e.touches[0].clientY;
|
||||||
|
dragCurrentY.current = e.touches[0].clientY;
|
||||||
|
dragStartTime.current = Date.now();
|
||||||
|
if (drawerRef.current) {
|
||||||
|
drawerRef.current.style.transition = 'none';
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleTouchMove = useCallback((e) => {
|
||||||
|
if (dragStartY.current === null) return;
|
||||||
|
dragCurrentY.current = e.touches[0].clientY;
|
||||||
|
const dy = dragCurrentY.current - dragStartY.current;
|
||||||
|
if (dy > 0 && drawerRef.current) {
|
||||||
|
drawerRef.current.style.transform = `translateY(${dy}px)`;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleTouchEnd = useCallback(() => {
|
||||||
|
if (dragStartY.current === null || !drawerRef.current) return;
|
||||||
|
const dy = dragCurrentY.current - dragStartY.current;
|
||||||
|
const dt = (Date.now() - dragStartTime.current) / 1000;
|
||||||
|
const velocity = dt > 0 ? dy / dt : 0;
|
||||||
|
const drawerHeight = drawerRef.current.offsetHeight;
|
||||||
|
const threshold = drawerHeight * 0.3;
|
||||||
|
|
||||||
|
if (dy > threshold || velocity > 500) {
|
||||||
|
dismiss();
|
||||||
|
} else {
|
||||||
|
drawerRef.current.style.transition = 'transform 0.2s ease-out';
|
||||||
|
drawerRef.current.style.transform = 'translateY(0)';
|
||||||
|
}
|
||||||
|
dragStartY.current = null;
|
||||||
|
}, [dismiss]);
|
||||||
|
|
||||||
|
const isPinned = message?.pinned;
|
||||||
|
|
||||||
|
return ReactDOM.createPortal(
|
||||||
|
<>
|
||||||
|
<div className="mobile-drawer-overlay" onClick={dismiss} />
|
||||||
|
<div
|
||||||
|
ref={drawerRef}
|
||||||
|
className={`mobile-drawer${closing ? ' mobile-drawer-closing' : ''}`}
|
||||||
|
onTouchStart={handleTouchStart}
|
||||||
|
onTouchMove={handleTouchMove}
|
||||||
|
onTouchEnd={handleTouchEnd}
|
||||||
|
>
|
||||||
|
<div className="mobile-drawer-handle">
|
||||||
|
<div className="mobile-drawer-handle-bar" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Quick reactions */}
|
||||||
|
<div className="mobile-drawer-reactions">
|
||||||
|
{QUICK_REACTIONS.map(({ name, category }) => (
|
||||||
|
<button
|
||||||
|
key={name}
|
||||||
|
className="mobile-drawer-reaction-btn"
|
||||||
|
onClick={() => handleQuickReaction(name)}
|
||||||
|
>
|
||||||
|
<ColoredIcon src={getEmojiUrl(category, name)} size="24px" color={null} />
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
<button
|
||||||
|
className="mobile-drawer-reaction-btn"
|
||||||
|
onClick={() => handleAction('reaction')}
|
||||||
|
>
|
||||||
|
<ColoredIcon src={EmojieIcon} color={ICON_COLOR_DEFAULT} size="22px" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mobile-drawer-separator" />
|
||||||
|
|
||||||
|
{/* Primary actions */}
|
||||||
|
<div className="mobile-drawer-card">
|
||||||
|
{isOwner && !isAttachment && (
|
||||||
|
<button className="mobile-drawer-action" onClick={() => handleAction('edit')}>
|
||||||
|
<ColoredIcon src={EditIcon} color={ICON_COLOR_DEFAULT} size="20px" />
|
||||||
|
<span>Edit Message</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button className="mobile-drawer-action" onClick={() => handleAction('reply')}>
|
||||||
|
<ColoredIcon src={ReplyIcon} color={ICON_COLOR_DEFAULT} size="20px" />
|
||||||
|
<span>Reply</span>
|
||||||
|
</button>
|
||||||
|
{!isAttachment && (
|
||||||
|
<button className="mobile-drawer-action" onClick={() => handleAction('copy')}>
|
||||||
|
<CopyIcon />
|
||||||
|
<span>Copy Text</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button className="mobile-drawer-action" onClick={() => handleAction('pin')}>
|
||||||
|
<ColoredIcon src={PinIcon} color={ICON_COLOR_DEFAULT} size="20px" />
|
||||||
|
<span>{isPinned ? 'Unpin Message' : 'Pin Message'}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Danger actions */}
|
||||||
|
{canDelete && (
|
||||||
|
<div className="mobile-drawer-card">
|
||||||
|
<button className="mobile-drawer-action mobile-drawer-action-danger" onClick={() => handleAction('delete')}>
|
||||||
|
<ColoredIcon src={DeleteIcon} color="#ed4245" size="20px" />
|
||||||
|
<span>Delete Message</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>,
|
||||||
|
document.body
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MobileMessageDrawer;
|
||||||
@@ -3417,6 +3417,134 @@ body {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
MOBILE MESSAGE ACTION DRAWER
|
||||||
|
============================================ */
|
||||||
|
|
||||||
|
@keyframes mobileDrawerOverlayIn {
|
||||||
|
from { opacity: 0; }
|
||||||
|
to { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes mobileDrawerSlideUp {
|
||||||
|
from { transform: translateY(100%); }
|
||||||
|
to { transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes mobileDrawerSlideDown {
|
||||||
|
from { transform: translateY(0); }
|
||||||
|
to { transform: translateY(100%); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-drawer-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.6);
|
||||||
|
z-index: 10002;
|
||||||
|
animation: mobileDrawerOverlayIn 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-drawer {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
max-height: 70vh;
|
||||||
|
background: #1C1D22;
|
||||||
|
border-radius: 16px 16px 0 0;
|
||||||
|
z-index: 10003;
|
||||||
|
animation: mobileDrawerSlideUp 0.25s ease-out;
|
||||||
|
padding-bottom: env(safe-area-inset-bottom, 0px);
|
||||||
|
touch-action: none;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-drawer.mobile-drawer-closing {
|
||||||
|
animation: mobileDrawerSlideDown 0.2s ease-in forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-drawer-handle {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 10px 0 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-drawer-handle-bar {
|
||||||
|
width: 40px;
|
||||||
|
height: 4px;
|
||||||
|
border-radius: 2px;
|
||||||
|
background: hsla(240, 4%, 60%, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-drawer-reactions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px 16px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-drawer-reaction-btn {
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #2E3138;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
border: none;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
transition: background-color 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-drawer-reaction-btn:active {
|
||||||
|
background: hsla(240, 4%, 40%, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-drawer-separator {
|
||||||
|
height: 1px;
|
||||||
|
background: hsla(240, 4%, 60%, 0.12);
|
||||||
|
margin: 0 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-drawer-card {
|
||||||
|
margin: 8px 16px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: #27272F;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-drawer-action {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
color: var(--text-normal, #dbdee1);
|
||||||
|
font-size: 15px;
|
||||||
|
cursor: pointer;
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-drawer-action:active {
|
||||||
|
background: hsla(240, 4%, 60%, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-drawer-action + .mobile-drawer-action {
|
||||||
|
border-top: 1px solid hsla(240, 4%, 60%, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-drawer-action-danger {
|
||||||
|
color: #ed4245;
|
||||||
|
}
|
||||||
|
|
||||||
/* Remove border between server-list and channel-list on mobile */
|
/* Remove border between server-list and channel-list on mobile */
|
||||||
.is-mobile .server-list {
|
.is-mobile .server-list {
|
||||||
border-right: none;
|
border-right: none;
|
||||||
|
|||||||
Reference in New Issue
Block a user