Upload/Download progress; Mobile view
This commit is contained in:
parent
6b6bbdc142
commit
c1f24e3d41
35 changed files with 491 additions and 180 deletions
4
package-lock.json
generated
4
package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "pulsar-web",
|
||||
"version": "0.6.0",
|
||||
"version": "0.7.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "pulsar-web",
|
||||
"version": "0.6.0",
|
||||
"version": "0.7.1",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@solidjs/router": "^0.15.4",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "pulsar-web",
|
||||
"version": "0.7.0",
|
||||
"version": "0.7.1",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
|
|
|||
|
|
@ -1,15 +1,30 @@
|
|||
import type { Component } from "solid-js";
|
||||
import { IChannelBarProps } from "./types";
|
||||
import { LeftIcon, UserIcon } from "../../icons";
|
||||
import { resetActiveChannel } from "../../store/channel";
|
||||
import { setMembersOpenMobile } from "../../store/app";
|
||||
|
||||
const ChannelBar: Component<IChannelBarProps> = (props: IChannelBarProps) => {
|
||||
return (
|
||||
<div class="absolute w-full top-0 z-10">
|
||||
<div class="flex flex-col justify-center bg-stone-900/25 backdrop-blur-md h-16 w-full shadow-bar px-5">
|
||||
<div class="absolute w-full flex flex-row top-0 z-10 bg-stone-900/25 backdrop-blur-md h-16 shadow-bar">
|
||||
<div
|
||||
class="block lg:hidden h-full ml-1 w-16 p-5 cursor-pointer"
|
||||
onClick={resetActiveChannel}
|
||||
>
|
||||
<LeftIcon />
|
||||
</div>
|
||||
<div class="flex-1 flex flex-col justify-center px-5">
|
||||
<h2 class="text-sm font-bold">
|
||||
{props.name ? `# ${props.name}` : undefined}
|
||||
</h2>
|
||||
<p class="text-xs">{props.description}</p>
|
||||
</div>
|
||||
<div
|
||||
class="block lg:hidden h-full ml-1 w-16 p-5 cursor-pointer"
|
||||
onClick={() => setMembersOpenMobile(true)}
|
||||
>
|
||||
<UserIcon />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ const FileInput: Component<IFileInputProps> = (props: IFileInputProps) => {
|
|||
class={`bg-stone-800 h-40 w-40 p-2 ${props.rounded ? "rounded-full" : "rounded-2xl"} ${props.outline ? "outline-2" : ""}`}
|
||||
>
|
||||
<label
|
||||
class={`relative inline-block bg-stone-900 input w-full h-full p-0 focus:border-none outline-none border-2 border-stone-700 cursor-pointer ${props.rounded ? "rounded-full" : "rounded-xl"}`}
|
||||
class={`relative inline-block bg-stone-900 input w-full h-full p-0 border-none outline-2 outline-stone-700 cursor-pointer ${props.rounded ? "rounded-full" : "rounded-xl"}`}
|
||||
>
|
||||
{props.picture ? pictureHtml() : iconOnlyHtml()}
|
||||
<input
|
||||
|
|
|
|||
|
|
@ -1,37 +1,58 @@
|
|||
import { type Component } from "solid-js";
|
||||
import { JSXElement, type Component } from "solid-js";
|
||||
import { IFilePreviewProps } from "./types";
|
||||
import { Dynamic } from "solid-js/web";
|
||||
|
||||
const FilePreview: Component<IFilePreviewProps> = (
|
||||
props: IFilePreviewProps,
|
||||
) => {
|
||||
const iconHtml = (): JSXElement => (
|
||||
<div class="w-full h-full flex flex-col items-center justify-center min-w-0">
|
||||
<div class="w-12">
|
||||
<Dynamic component={props.icon} />
|
||||
</div>
|
||||
<p class="w-full text-center px-2 truncate">{props.filename}</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
const pictureHtml = (): JSXElement => (
|
||||
<div class="avatar w-full h-full">
|
||||
<img
|
||||
class={props.rounded ? "rounded-full" : "rounded-xl"}
|
||||
src={props.picture}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
const downloadHtml = (): JSXElement => (
|
||||
<div class="w-full h-full flex flex-col items-center justify-center min-w-0">
|
||||
<div
|
||||
class="radial-progress"
|
||||
style={{
|
||||
"--value": props.downloadProgress ?? 0,
|
||||
"--size": "4rem",
|
||||
"--thickness": "7px",
|
||||
}}
|
||||
role="progressbar"
|
||||
>
|
||||
{(props.downloadProgress ?? 0).toFixed(0)}%
|
||||
</div>
|
||||
<p class="w-full text-center px-2 truncate">{props.filename}</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
class={`bg-stone-800 p-2 ${props.allowResize ? "min-h-40 min-w-40 max-h-fit max-w-82" : "h-40 w-40"} ${props.rounded ? "rounded-full" : "rounded-2xl"} ${props.outline ? "outline-2" : ""}`}
|
||||
>
|
||||
<label
|
||||
class={`relative inline-block bg-stone-900 w-full h-full p-0 border-2 border-stone-700 focus:border-none outline-none ${props.clickable ? "cursor-pointer" : "cursor-default"} ${props.rounded ? "rounded-full" : "rounded-xl"}`}
|
||||
class={`relative inline-block bg-stone-900 w-full h-full p-0 outline-2 outline-stone-700 ${props.clickable && props.downloadProgress === undefined ? "cursor-pointer" : "cursor-default"} ${props.rounded ? "rounded-full" : "rounded-xl"}`}
|
||||
>
|
||||
{props.picture ? (
|
||||
<div class="avatar w-full h-full">
|
||||
<img
|
||||
class={
|
||||
props.rounded ? "rounded-full" : "rounded-xl"
|
||||
}
|
||||
src={props.picture}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div class="w-full h-full flex flex-col items-center justify-center min-w-0">
|
||||
<div class="w-12">
|
||||
<Dynamic component={props.icon} />
|
||||
</div>
|
||||
<p class="w-full text-center px-2 truncate">
|
||||
{props.filename}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{props.clickable ? (
|
||||
{props.downloadProgress === undefined
|
||||
? props.picture
|
||||
? pictureHtml()
|
||||
: iconHtml()
|
||||
: downloadHtml()}
|
||||
{props.clickable && props.downloadProgress === undefined ? (
|
||||
<div
|
||||
class={`absolute inset-0 flex items-center justify-center bg-black/50 text-white transition-opacity opacity-0 hover:opacity-100 ${props.rounded ? "rounded-full" : "rounded-xl"}`}
|
||||
onClick={() => props.onClick?.(props.id)}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ interface IFilePreviewProps {
|
|||
outline?: boolean;
|
||||
allowResize?: boolean;
|
||||
clickable?: boolean;
|
||||
downloadProgress?: number;
|
||||
clickIcon?: (props: IconParameters) => JSXElement;
|
||||
onClick?: (id: string | number) => void;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ import { Dynamic } from "solid-js/web";
|
|||
|
||||
const HomeCard: Component<IHomeCardProps> = (props: IHomeCardProps) => {
|
||||
return (
|
||||
<a class="w-60 cursor-pointer" onClick={props.onClick}>
|
||||
<div class="card outline-2 bg-stone-800 outline-stone-500 hover:outline-stone-100 w-60 h-60">
|
||||
<a class="cursor-pointer" onClick={props.onClick}>
|
||||
<div class="card transition-all border-2 bg-stone-800 border-stone-500 hover:border-stone-100 w-60 h-60">
|
||||
<div class="flex flex-col h-full gap-1 m-6">
|
||||
<div class="w-20">
|
||||
<Dynamic component={props.icon} />
|
||||
|
|
|
|||
|
|
@ -13,14 +13,20 @@ import { FilePreview } from "../FilePreview";
|
|||
import { fetchFile } from "../../services/file";
|
||||
import { getActiveCommunity } from "../../store/community";
|
||||
import { Input } from "../Input";
|
||||
import { IAttachment } from "../../store/file";
|
||||
|
||||
const Message: Component<IMessageProps> = (props: IMessageProps) => {
|
||||
const [getEditedText, setEditedText] = createSignal<string>(props.message);
|
||||
const [getEditedText, setEditedText] = createSignal<string>(
|
||||
props.message.text,
|
||||
);
|
||||
const [getDownloadProgress, setDownloadProgress] = createSignal<
|
||||
Map<number, number>
|
||||
>(new Map<number, number>());
|
||||
|
||||
const avatarHtml = (): JSXElement => (
|
||||
<div
|
||||
class="avatar cursor-pointer"
|
||||
onClick={() => props.onProfileClick?.(props.userId)}
|
||||
onClick={() => props.onProfileClick?.(props.message.ownerId)}
|
||||
>
|
||||
{props.avatar ? (
|
||||
<div class="w-10 h-10 rounded-full">
|
||||
|
|
@ -35,7 +41,7 @@ const Message: Component<IMessageProps> = (props: IMessageProps) => {
|
|||
);
|
||||
|
||||
const onUpdateMessage = () => {
|
||||
props.onMessageEdit?.(props.messageId, getEditedText());
|
||||
props.onMessageEdit?.(props.message.id, getEditedText());
|
||||
};
|
||||
|
||||
const onDownloadAttachment = (attachmentId: string | number) => {
|
||||
|
|
@ -44,9 +50,36 @@ const Message: Component<IMessageProps> = (props: IMessageProps) => {
|
|||
return;
|
||||
}
|
||||
|
||||
const updateProgress = (chunkIndex: number, totalChunks: number) => {
|
||||
const fileProgress = (chunkIndex + 1) / totalChunks;
|
||||
|
||||
if (fileProgress === 1) {
|
||||
setDownloadProgress((original) => {
|
||||
const next = new Map(original);
|
||||
next.delete(Number(attachmentId));
|
||||
|
||||
return next;
|
||||
});
|
||||
} else {
|
||||
setDownloadProgress((original) => {
|
||||
const next = new Map(original);
|
||||
next.set(Number(attachmentId), fileProgress * 100);
|
||||
|
||||
return next;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const community = getActiveCommunity();
|
||||
if (community) {
|
||||
fetchFile(community.id, attachmentId.toString());
|
||||
setDownloadProgress((original) => {
|
||||
const next = new Map(original);
|
||||
next.set(Number(attachmentId), 0);
|
||||
|
||||
return next;
|
||||
});
|
||||
|
||||
fetchFile(community.id, attachmentId.toString(), updateProgress);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -77,11 +110,14 @@ const Message: Component<IMessageProps> = (props: IMessageProps) => {
|
|||
}
|
||||
|
||||
if (!attachment.mimetype.startsWith("image/")) {
|
||||
const progress = getDownloadProgress().get(Number(attachment.id));
|
||||
|
||||
return (
|
||||
<FilePreview
|
||||
id={attachmentId}
|
||||
icon={FileIcon}
|
||||
filename={attachment.filename}
|
||||
downloadProgress={progress}
|
||||
clickable={true}
|
||||
clickIcon={DownloadIcon}
|
||||
onClick={onDownloadAttachment}
|
||||
|
|
@ -103,7 +139,7 @@ const Message: Component<IMessageProps> = (props: IMessageProps) => {
|
|||
<FilePreview
|
||||
id={attachmentId}
|
||||
picture={URL.createObjectURL(attachment.fullFile)}
|
||||
allowResize={props.attachments.length === 1}
|
||||
allowResize={props.message.attachments.length === 1}
|
||||
clickable={true}
|
||||
clickIcon={ZoomIcon}
|
||||
onClick={onOpenAttachment}
|
||||
|
|
@ -113,7 +149,7 @@ const Message: Component<IMessageProps> = (props: IMessageProps) => {
|
|||
|
||||
const attachmentsHtml = (): JSXElement => (
|
||||
<div class="mt-2 flex flex-row flex-wrap gap-2">
|
||||
{props.attachments.map(mapAttachment)}
|
||||
{props.message.attachments.map(mapAttachment)}
|
||||
</div>
|
||||
);
|
||||
|
||||
|
|
@ -123,14 +159,18 @@ const Message: Component<IMessageProps> = (props: IMessageProps) => {
|
|||
</div>
|
||||
);
|
||||
|
||||
const timeHtml = (): JSXElement => (
|
||||
<div class="flex-1 text-[0.6rem] opacity-40 text-end text-nowrap">
|
||||
{new Date(props.time).toLocaleTimeString()}
|
||||
const statusHtml = (): JSXElement => (
|
||||
<div class="h-full flex flex-col justify-between text-[0.6rem] opacity-40">
|
||||
<div class="text-nowrap">
|
||||
{props.message.notSentYet
|
||||
? "Sending..."
|
||||
: new Date(props.message.creationDate).toLocaleTimeString()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const editingHtml = (): JSXElement => (
|
||||
<div class="">
|
||||
<div>
|
||||
<Input
|
||||
value={getEditedText()}
|
||||
onChange={setEditedText}
|
||||
|
|
@ -141,9 +181,10 @@ const Message: Component<IMessageProps> = (props: IMessageProps) => {
|
|||
|
||||
const messageHtml = (): JSXElement => (
|
||||
<>
|
||||
{props.message.length === 0 ? undefined : props.decryptionStatus ? (
|
||||
{props.message.text.length === 0 ? undefined : props.message
|
||||
.decryptionStatus ? (
|
||||
<p class="list-col-wrap text-xs">
|
||||
{props.editing ? editingHtml() : props.message}
|
||||
{props.editing ? editingHtml() : props.message.text}
|
||||
</p>
|
||||
) : (
|
||||
<p class="list-col-wrap text-xs italic opacity-75">
|
||||
|
|
@ -157,16 +198,18 @@ const Message: Component<IMessageProps> = (props: IMessageProps) => {
|
|||
<li
|
||||
class={`flex list-row hover:bg-stone-700 after:hidden ${props.plain ? "py-1" : "py-2 mt-3"} ${props.plain ? "" : "before:absolute before:top-0 before:left-2 before:right-2 before:h-px before:bg-stone-700 first:before:hidden"}`}
|
||||
onContextMenu={(e) =>
|
||||
props.onMessageRightClick?.(props.messageId, e)
|
||||
props.onMessageRightClick?.(props.message.id, e)
|
||||
}
|
||||
>
|
||||
{props.plain ? undefined : avatarHtml()}
|
||||
<div class={`w-full pr-14 ${props.plain ? "pl-14" : ""}`}>
|
||||
<div class={`w-full pr-4 ${props.plain ? "pl-14" : ""}`}>
|
||||
{props.plain ? undefined : nameHtml()}
|
||||
{messageHtml()}
|
||||
{props.attachments.length > 0 ? attachmentsHtml() : undefined}
|
||||
{props.message.attachments.length > 0
|
||||
? attachmentsHtml()
|
||||
: undefined}
|
||||
</div>
|
||||
{timeHtml()}
|
||||
{statusHtml()}
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,14 +1,11 @@
|
|||
import { IMessage } from "../../store/message";
|
||||
|
||||
interface IMessageProps {
|
||||
messageId: string;
|
||||
message: string;
|
||||
userId: string;
|
||||
message: IMessage;
|
||||
username: string;
|
||||
color?: string;
|
||||
avatar?: string;
|
||||
attachments: string[];
|
||||
time: number;
|
||||
plain: boolean;
|
||||
decryptionStatus: boolean;
|
||||
editing: boolean;
|
||||
onProfileClick?: (userId: string) => void;
|
||||
onMessageRightClick?: (id: string, event: MouseEvent) => void;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,13 @@
|
|||
import { createSignal, type Component, type JSXElement } from "solid-js";
|
||||
import {
|
||||
createEffect,
|
||||
createSignal,
|
||||
type Component,
|
||||
type JSXElement,
|
||||
} from "solid-js";
|
||||
import { IMessageBarProps, MESSAGE_BAR_OPTIONS, OptionIcon } from "./types";
|
||||
import {
|
||||
AttachmentIcon,
|
||||
LocationIcon,
|
||||
PlusIcon,
|
||||
PollIcon,
|
||||
TrashIcon,
|
||||
|
|
@ -24,9 +30,19 @@ const MessageBar: Component<IMessageBarProps> = (props: IMessageBarProps) => {
|
|||
|
||||
const options = new Map<MESSAGE_BAR_OPTIONS, OptionIcon>([
|
||||
[MESSAGE_BAR_OPTIONS.ATTACH_FILES, AttachmentIcon],
|
||||
[MESSAGE_BAR_OPTIONS.ATTACH_LOCATION, LocationIcon],
|
||||
[MESSAGE_BAR_OPTIONS.CREATE_POLL, PollIcon],
|
||||
]);
|
||||
|
||||
createEffect(() => {
|
||||
if (props.uploadProgress === undefined) {
|
||||
setAttachmentOpen(false);
|
||||
|
||||
setFiles([]);
|
||||
setFilePreviews([]);
|
||||
}
|
||||
});
|
||||
|
||||
const handleEnter = (e: KeyboardEvent) => {
|
||||
if (e.key === "Enter") {
|
||||
onSend();
|
||||
|
|
@ -95,10 +111,6 @@ const MessageBar: Component<IMessageBarProps> = (props: IMessageBarProps) => {
|
|||
props.onSend?.(getFiles());
|
||||
|
||||
setDropdownOpen(false);
|
||||
setAttachmentOpen(false);
|
||||
|
||||
setFiles([]);
|
||||
setFilePreviews([]);
|
||||
};
|
||||
|
||||
const mapOption = (
|
||||
|
|
@ -126,7 +138,7 @@ const MessageBar: Component<IMessageBarProps> = (props: IMessageBarProps) => {
|
|||
id={index}
|
||||
icon={AttachmentIcon}
|
||||
filename={getFiles().at(index)?.name}
|
||||
clickable={true}
|
||||
clickable={!props.uploadProgress}
|
||||
clickIcon={TrashIcon}
|
||||
picture={filePreview}
|
||||
onClick={onFileRemove}
|
||||
|
|
@ -139,7 +151,48 @@ const MessageBar: Component<IMessageBarProps> = (props: IMessageBarProps) => {
|
|||
<div class="bg-stone-800/50 backdrop-blur-lg h-48 w-full shadow-bar rounded-3xl p-4 flex flex-row gap-2 overflow-x-scroll">
|
||||
{getFilePreviews().map(mapFile)}
|
||||
<div class="w-40">
|
||||
<FileInput multifile={true} onChange={onFileAdd} />
|
||||
<FileInput
|
||||
multifile={true}
|
||||
onChange={(fileList) =>
|
||||
!props.uploadProgress
|
||||
? onFileAdd?.(fileList)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const uploadingHtml = (): JSXElement => (
|
||||
<div class="absolute w-full bottom-18 p-4 z-10">
|
||||
<div class="flex flex-row gap-2 bg-stone-800/50 backdrop-blur-lg h-16 w-64 shadow-bar rounded-full mx-auto p-1">
|
||||
<div
|
||||
class="radial-progress"
|
||||
style={{
|
||||
"--value": props.uploadProgress?.total,
|
||||
"--size": "3.5rem",
|
||||
"--thickness": "7px",
|
||||
}}
|
||||
role="progressbar"
|
||||
>
|
||||
{props.uploadProgress?.total.toFixed(0)}%
|
||||
</div>
|
||||
<div class="flex-1 flex flex-col justify-center items-center">
|
||||
{props.uploadProgress?.total === 0 ? (
|
||||
<span>Starting upload...</span>
|
||||
) : (
|
||||
<span>
|
||||
Uploading file {props.uploadProgress?.fileIndex}/
|
||||
{props.uploadProgress?.fileCount}
|
||||
...
|
||||
</span>
|
||||
)}
|
||||
{props.uploadProgress?.total === 0 ? undefined : (
|
||||
<span>
|
||||
{props.uploadProgress?.fileProgress.toFixed(0)}%
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -147,11 +200,16 @@ const MessageBar: Component<IMessageBarProps> = (props: IMessageBarProps) => {
|
|||
|
||||
return (
|
||||
<>
|
||||
{getAttachmentOpen() ? attachmentHtml() : undefined}
|
||||
<div class="absolute w-full bottom-0 p-4 z-10">
|
||||
<div class="absolute h-16 w-full pr-8">
|
||||
{getAttachmentOpen()
|
||||
? props.uploadProgress
|
||||
? uploadingHtml()
|
||||
: attachmentHtml()
|
||||
: undefined}
|
||||
<div class="absolute w-full bottom-0 z-10">
|
||||
<div class="absolute h-24 w-full p-4">
|
||||
<div class="bg-stone-800/25 backdrop-blur-lg rounded-full h-full w-full"></div>
|
||||
</div>
|
||||
<div class="p-4">
|
||||
<div class="bg-stone-800/25 h-16 shadow-bar p-2 flex flex-row gap-2 rounded-full">
|
||||
<div
|
||||
class={`group dropdown dropdown-top ${getDropdownOpen() ? "" : "dropdown-close"}`}
|
||||
|
|
@ -159,8 +217,12 @@ const MessageBar: Component<IMessageBarProps> = (props: IMessageBarProps) => {
|
|||
<div
|
||||
tabindex="0"
|
||||
role="button"
|
||||
class={`btn btn-neutral bg-stone-950/50 backdrop-blur-lg w-12 p-0 h-full rounded-full transition-transform ${getDropdownOpen() ? "rotate-45" : ""}`}
|
||||
onClick={openDropdown}
|
||||
class={`btn ${props.uploadProgress ? "btn-disabled" : ""} btn-neutral bg-stone-950/50 backdrop-blur-lg w-12 p-0 h-full rounded-full transition-transform ${getDropdownOpen() ? "rotate-45" : ""}`}
|
||||
onClick={() =>
|
||||
props.uploadProgress
|
||||
? undefined
|
||||
: openDropdown?.()
|
||||
}
|
||||
>
|
||||
<div class="w-6">
|
||||
<PlusIcon />
|
||||
|
|
@ -181,6 +243,7 @@ const MessageBar: Component<IMessageBarProps> = (props: IMessageBarProps) => {
|
|||
type="text"
|
||||
placeholder="Send a message..."
|
||||
value={props.text}
|
||||
disabled={props.uploadProgress !== undefined}
|
||||
onInput={(e) =>
|
||||
props.onChangeText?.(e.currentTarget.value)
|
||||
}
|
||||
|
|
@ -189,6 +252,7 @@ const MessageBar: Component<IMessageBarProps> = (props: IMessageBarProps) => {
|
|||
</label>
|
||||
<button
|
||||
class="bg-stone-950/50 backdrop-blur-lg btn btn-neutral w-12 p-0 h-full rounded-full"
|
||||
disabled={props.uploadProgress !== undefined}
|
||||
onClick={onSend}
|
||||
>
|
||||
<div class="w-5">
|
||||
|
|
@ -197,6 +261,7 @@ const MessageBar: Component<IMessageBarProps> = (props: IMessageBarProps) => {
|
|||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,15 +3,30 @@ import { IconParameters } from "../../icons";
|
|||
|
||||
interface IMessageBarProps {
|
||||
text: string;
|
||||
uploadProgress?: IUploadProgress;
|
||||
onChangeText?: (text: string) => void;
|
||||
onSend?: (files: File[]) => void;
|
||||
}
|
||||
|
||||
interface IUploadProgress {
|
||||
total: number;
|
||||
fileName: string;
|
||||
fileIndex: number;
|
||||
fileCount: number;
|
||||
fileProgress: number;
|
||||
}
|
||||
|
||||
type OptionIcon = (props: IconParameters) => JSXElement;
|
||||
|
||||
enum MESSAGE_BAR_OPTIONS {
|
||||
ATTACH_FILES = "Attach files",
|
||||
ATTACH_LOCATION = "Attach location",
|
||||
CREATE_POLL = "Create a poll",
|
||||
}
|
||||
|
||||
export { type IMessageBarProps, type OptionIcon, MESSAGE_BAR_OPTIONS };
|
||||
export {
|
||||
type IMessageBarProps,
|
||||
type IUploadProgress,
|
||||
type OptionIcon,
|
||||
MESSAGE_BAR_OPTIONS,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ const RichSettingsItem: Component<IRichSettingsItemProps> = (
|
|||
if (props.avatar) {
|
||||
return (
|
||||
<div class="avatar">
|
||||
<div class="w-12 rounded-lg">
|
||||
<div class="min-w-12 w-12 rounded-lg">
|
||||
<img src={props.avatar} />
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -19,7 +19,7 @@ const RichSettingsItem: Component<IRichSettingsItemProps> = (
|
|||
if (props.icon) {
|
||||
return (
|
||||
<div
|
||||
class={`bg-stone-700 w-12 rounded-lg p-${props.iconPadding ? props.iconPadding : 1}`}
|
||||
class={`bg-stone-700 min-w-12 w-12 rounded-lg p-${props.iconPadding ? props.iconPadding : 1}`}
|
||||
>
|
||||
<Dynamic component={props.icon} />
|
||||
</div>
|
||||
|
|
@ -51,7 +51,7 @@ const RichSettingsItem: Component<IRichSettingsItemProps> = (
|
|||
<input type="checkbox" />
|
||||
<div class="collapse-title font-semibold flex flex-row items-center gap-4 p-1">
|
||||
{pictureHtml()}
|
||||
{props.title}
|
||||
<span class="truncate">{props.title}</span>
|
||||
{infoHtml()}
|
||||
</div>
|
||||
<div class="collapse-content text-sm font-semibold">
|
||||
|
|
|
|||
|
|
@ -2,9 +2,10 @@ import { state } from "../../store/state";
|
|||
import { decryptData, encryptData, importKey } from "../crypto";
|
||||
import { DB_STORE, IDatabaseItem } from "./types";
|
||||
|
||||
const DB_NAME = "pulsardb";
|
||||
const DB_VERSION = 1;
|
||||
|
||||
const openDB = async (name: string, store: string): Promise<IDBDatabase> => {
|
||||
const openDB = async (name: string): Promise<IDBDatabase> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = indexedDB.open(name, DB_VERSION);
|
||||
|
||||
|
|
@ -27,12 +28,16 @@ const openDB = async (name: string, store: string): Promise<IDBDatabase> => {
|
|||
});
|
||||
};
|
||||
|
||||
const getDB = async (store: string): Promise<IDBDatabase> => {
|
||||
return await openDB("pulsardb", store);
|
||||
const getDB = async (): Promise<IDBDatabase> => {
|
||||
return await openDB(DB_NAME);
|
||||
};
|
||||
|
||||
const deleteDB = async () => {
|
||||
return indexedDB.deleteDatabase(DB_NAME);
|
||||
};
|
||||
|
||||
const dbSave = async (store: string, item: IDatabaseItem): Promise<void> => {
|
||||
const db = await getDB(store);
|
||||
const db = await getDB();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const tx = db.transaction(store, "readwrite");
|
||||
|
|
@ -47,7 +52,7 @@ const dbLoad = async (
|
|||
store: string,
|
||||
id: IDBValidKey,
|
||||
): Promise<IDatabaseItem | undefined> => {
|
||||
const db = await getDB(store);
|
||||
const db = await getDB();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const tx = db.transaction(store, "readonly");
|
||||
|
|
@ -110,6 +115,7 @@ const dbLoadEncrypted = async <T>(
|
|||
export {
|
||||
openDB,
|
||||
getDB,
|
||||
deleteDB,
|
||||
dbSave,
|
||||
dbLoad,
|
||||
importKey,
|
||||
|
|
|
|||
|
|
@ -27,7 +27,8 @@ import {
|
|||
generateIv,
|
||||
} from "../crypto";
|
||||
|
||||
const CHUNK_SIZE = 1024 * 1024 * 5;
|
||||
const MIN_CHUNK_SIZE = 1024 * 1024 * 1;
|
||||
const MAX_CHUNK_SIZE = 1024 * 1024 * 5;
|
||||
|
||||
const uploadUserAvatar = async (filename: string, file: File) => {
|
||||
const data = await uploadUserAvatarApi({ filename: filename, file: file });
|
||||
|
|
@ -153,7 +154,13 @@ const createAttachment = async (
|
|||
return;
|
||||
}
|
||||
|
||||
setAttachment({ ...data, fullFile: undefined });
|
||||
setAttachment({
|
||||
...data,
|
||||
filename: filename,
|
||||
mimetype: mimetype,
|
||||
size: size,
|
||||
fullFile: undefined,
|
||||
});
|
||||
|
||||
return data.id;
|
||||
} catch {
|
||||
|
|
@ -215,6 +222,7 @@ const fetchChunks = async (
|
|||
attachmentId: string,
|
||||
chunkCount: number,
|
||||
mimetype?: string,
|
||||
onChunkDownloaded?: (chunkIndex: number, totalChunks: number) => void,
|
||||
) => {
|
||||
const file: ArrayBuffer[] = [];
|
||||
|
||||
|
|
@ -229,6 +237,8 @@ const fetchChunks = async (
|
|||
if (!status) {
|
||||
decryptionSuccessful = false;
|
||||
}
|
||||
|
||||
onChunkDownloaded?.(i, chunkCount);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
|
@ -242,13 +252,23 @@ const fetchChunks = async (
|
|||
setAttachmentDecryptionStatus(attachmentId, decryptionSuccessful);
|
||||
};
|
||||
|
||||
const fetchFile = async (communityId: string, attachmentId: string) => {
|
||||
const fetchFile = async (
|
||||
communityId: string,
|
||||
attachmentId: string,
|
||||
onChunkDownloaded?: (chunkIndex: number, totalChunks: number) => void,
|
||||
) => {
|
||||
const attachment = await fetchAttachment(attachmentId, communityId);
|
||||
if (!attachment) {
|
||||
return;
|
||||
}
|
||||
|
||||
await fetchChunks(communityId, attachmentId, attachment.chunks.length);
|
||||
await fetchChunks(
|
||||
communityId,
|
||||
attachmentId,
|
||||
attachment.chunks.length,
|
||||
undefined,
|
||||
onChunkDownloaded,
|
||||
);
|
||||
|
||||
const updatedAttachment = state.file.attachments[attachmentId];
|
||||
if (updatedAttachment?.fullFile && updatedAttachment.decryptionStatus) {
|
||||
|
|
@ -264,7 +284,11 @@ const fetchFile = async (communityId: string, attachmentId: string) => {
|
|||
}
|
||||
};
|
||||
|
||||
const fetchMedia = async (communityId: string, attachmentId: string) => {
|
||||
const fetchMedia = async (
|
||||
communityId: string,
|
||||
attachmentId: string,
|
||||
onChunkDownloaded?: (chunkIndex: number, totalChunks: number) => void,
|
||||
) => {
|
||||
const attachment = await fetchAttachment(attachmentId, communityId);
|
||||
if (!attachment) {
|
||||
return;
|
||||
|
|
@ -279,6 +303,7 @@ const fetchMedia = async (communityId: string, attachmentId: string) => {
|
|||
attachmentId,
|
||||
attachment.chunks.length,
|
||||
attachment.mimetype,
|
||||
onChunkDownloaded,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
@ -328,12 +353,22 @@ const uploadChunks = async (
|
|||
communityId: string,
|
||||
attachmentId: string,
|
||||
file: File,
|
||||
onChunkUploaded?: (chunkIndex: number, totalChunks: number) => void,
|
||||
): Promise<boolean> => {
|
||||
const totalChunks = Math.ceil(file.size / CHUNK_SIZE);
|
||||
const fileSizeMb = file.size / 1024 / 1024;
|
||||
|
||||
let chunkSize = MIN_CHUNK_SIZE;
|
||||
if (fileSizeMb >= 100) {
|
||||
chunkSize = MAX_CHUNK_SIZE;
|
||||
} else if (fileSizeMb >= 20) {
|
||||
chunkSize = file.size / 20;
|
||||
}
|
||||
|
||||
const totalChunks = Math.ceil(file.size / chunkSize);
|
||||
|
||||
for (let index = 0; index < totalChunks; index++) {
|
||||
const start = index * CHUNK_SIZE;
|
||||
const end = Math.min(start + CHUNK_SIZE, file.size);
|
||||
const start = index * chunkSize;
|
||||
const end = Math.min(start + chunkSize, file.size);
|
||||
const blob = file.slice(start, end);
|
||||
|
||||
const uploaded = await uploadChunk(
|
||||
|
|
@ -345,6 +380,7 @@ const uploadChunks = async (
|
|||
if (!uploaded) {
|
||||
return false;
|
||||
}
|
||||
onChunkUploaded?.(index, totalChunks);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ const fetchMessage = async (id: string, communityId: string) => {
|
|||
const createMessage = async (
|
||||
communityId: string,
|
||||
channelId: string,
|
||||
ownerId: string,
|
||||
text: string,
|
||||
attachments: string[],
|
||||
replyToId?: string,
|
||||
|
|
@ -56,6 +57,24 @@ const createMessage = async (
|
|||
}
|
||||
const [encryptedMessage, iv] = encrypted;
|
||||
|
||||
const tempUUID = crypto.randomUUID();
|
||||
|
||||
setMessage({
|
||||
id: tempUUID,
|
||||
iv: iv,
|
||||
text: text,
|
||||
attachments: attachments,
|
||||
edited: false,
|
||||
reactions: [],
|
||||
creationDate: new Date().getTime(),
|
||||
editHistory: [],
|
||||
replyToId: replyToId,
|
||||
channelId: channelId,
|
||||
ownerId: ownerId,
|
||||
decryptionStatus: true,
|
||||
notSentYet: true,
|
||||
});
|
||||
|
||||
const data = await createMessageApi({
|
||||
text: encryptedMessage,
|
||||
iv: iv,
|
||||
|
|
@ -67,15 +86,14 @@ const createMessage = async (
|
|||
if (typeof data.error === "string") {
|
||||
return;
|
||||
}
|
||||
|
||||
deleteChannelMessage(channelId, tempUUID);
|
||||
|
||||
setMessage({
|
||||
...data,
|
||||
text: text,
|
||||
decryptionStatus: true,
|
||||
});
|
||||
|
||||
attachments.forEach((attachmentId) => {
|
||||
deleteAttachment(attachmentId);
|
||||
});
|
||||
};
|
||||
|
||||
const updateMessage = async (id: string, text: string, communityId: string) => {
|
||||
|
|
|
|||
|
|
@ -52,11 +52,20 @@ const parseMessage = (data: string): SocketMessage | null => {
|
|||
const handleMessage = (message: SocketMessage) => {
|
||||
switch (message.type) {
|
||||
case SocketMessageTypes.SET_MESSAGE: {
|
||||
const communityId =
|
||||
state.channel.channels[message.payload.channelId]?.communityId;
|
||||
if (!communityId) {
|
||||
const channel = state.channel.channels[message.payload.channelId];
|
||||
const communityId = channel?.communityId;
|
||||
if (!channel || !communityId) {
|
||||
break;
|
||||
}
|
||||
if (channel.messages) {
|
||||
const exists = Object.values(channel.messages).some(
|
||||
(msg) => msg?.iv === message.payload.message.iv,
|
||||
);
|
||||
if (exists) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
decryptMessage(
|
||||
communityId,
|
||||
message.payload.message.text,
|
||||
|
|
|
|||
|
|
@ -54,6 +54,10 @@ const resetContextMenuOpenId = () => {
|
|||
setState("app", "contextMenuOpenId", null);
|
||||
};
|
||||
|
||||
const setMembersOpenMobile = (membersOpenMobile: boolean) => {
|
||||
setState("app", "membersOpenMobile", membersOpenMobile);
|
||||
};
|
||||
|
||||
export {
|
||||
setAnnouncement,
|
||||
setHomeOpen,
|
||||
|
|
@ -66,4 +70,5 @@ export {
|
|||
setCreateInviteOpen,
|
||||
setContextMenuOpenId,
|
||||
resetContextMenuOpenId,
|
||||
setMembersOpenMobile,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ interface IAppState {
|
|||
announcement: IAnnouncement;
|
||||
dialogsOpen: IDialogsOpen;
|
||||
contextMenuOpenId: string | null;
|
||||
membersOpenMobile: boolean;
|
||||
}
|
||||
|
||||
interface IAnnouncement {
|
||||
|
|
|
|||
|
|
@ -19,4 +19,15 @@ const deleteMessage = () => {
|
|||
setState("message", "message", undefined);
|
||||
};
|
||||
|
||||
export { setMessage, deleteMessage };
|
||||
const deleteChannelMessage = (messageId: string, channelId: string) => {
|
||||
setState(
|
||||
"channel",
|
||||
"channels",
|
||||
channelId,
|
||||
"messages",
|
||||
messageId,
|
||||
undefined,
|
||||
);
|
||||
};
|
||||
|
||||
export { setMessage, deleteMessage, deleteChannelMessage };
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ interface IMessage {
|
|||
channelId?: string;
|
||||
creationDate: number;
|
||||
decryptionStatus?: boolean;
|
||||
notSentYet?: boolean;
|
||||
}
|
||||
|
||||
export { type IMessageState, type IMessage };
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { createStore } from "solid-js/store";
|
||||
import { IState } from "./types";
|
||||
|
||||
const [state, setState] = createStore<IState>({
|
||||
const defaultState: IState = {
|
||||
app: {
|
||||
homeOpen: true,
|
||||
announcement: {
|
||||
|
|
@ -18,6 +18,7 @@ const [state, setState] = createStore<IState>({
|
|||
createInviteOpen: false,
|
||||
},
|
||||
contextMenuOpenId: null,
|
||||
membersOpenMobile: false,
|
||||
},
|
||||
auth: {
|
||||
registerSuccess: undefined,
|
||||
|
|
@ -52,6 +53,12 @@ const [state, setState] = createStore<IState>({
|
|||
attachments: {},
|
||||
chunks: {},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export { state, setState };
|
||||
const [state, setState] = createStore<IState>(defaultState);
|
||||
|
||||
const resetState = () => {
|
||||
setState(defaultState);
|
||||
};
|
||||
|
||||
export { state, setState, resetState };
|
||||
|
|
|
|||
|
|
@ -38,7 +38,6 @@ const AppView: Component = () => {
|
|||
});
|
||||
|
||||
createEffect(() => {
|
||||
console.log(state.auth.loggedIn);
|
||||
if (state.auth.loggedIn === false) {
|
||||
navigate("/login");
|
||||
} else if (state.auth.loggedIn === true) {
|
||||
|
|
|
|||
|
|
@ -335,7 +335,9 @@ const ChannelView: Component = () => {
|
|||
};
|
||||
|
||||
return (
|
||||
<div class="bg-stone-900 w-80 shadow-panel z-20 h-full relative">
|
||||
<div
|
||||
class={`${getActiveChannel() ? "hidden lg:block" : "block flex-1 lg:flex-none"} lg:w-80 bg-stone-900 shadow-panel z-20 h-full relative`}
|
||||
>
|
||||
<ContextMenu
|
||||
visible={listMenu.getVisible()}
|
||||
position={listMenu.getPos()}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import {
|
|||
type Component,
|
||||
} from "solid-js";
|
||||
import { ChannelBar } from "../../components/ChannelBar";
|
||||
import { MessageBar } from "../../components/MessageBar";
|
||||
import { IUploadProgress, MessageBar } from "../../components/MessageBar";
|
||||
import { Message } from "../../components/Message";
|
||||
import { state } from "../../store/state";
|
||||
import { getActiveChannel, IChannel, setText } from "../../store/channel";
|
||||
|
|
@ -20,6 +20,7 @@ import {
|
|||
import { IMessage } from "../../store/message";
|
||||
import {
|
||||
createAttachment,
|
||||
fetchMedia,
|
||||
finishAttachment,
|
||||
uploadChunks,
|
||||
} from "../../services/file";
|
||||
|
|
@ -38,6 +39,7 @@ import {
|
|||
} from "../../icons";
|
||||
import { getUserBestRoleId } from "../../services/role";
|
||||
import { IRole } from "../../store/role";
|
||||
import { getLoggedInUser } from "../../store/user";
|
||||
|
||||
const ChatView: Component = () => {
|
||||
const menu = useContextMenu<string>("chat-message-menu");
|
||||
|
|
@ -45,6 +47,8 @@ const ChatView: Component = () => {
|
|||
const [getEditingMessages, setEditingMessages] = createSignal<Set<string>>(
|
||||
new Set(),
|
||||
);
|
||||
const [getAttachmentUploadProgress, setAttachmentUploadProgress] =
|
||||
createSignal<IUploadProgress | undefined>();
|
||||
|
||||
const editingMenuItems = createMemo<IContextMenuItem[]>(() => {
|
||||
if (getEditingMessages().has(menu.getData() ?? "")) {
|
||||
|
|
@ -180,7 +184,8 @@ const ChatView: Component = () => {
|
|||
|
||||
const channel = channelInfo();
|
||||
const community = getActiveCommunity();
|
||||
if (!channel?.id || !community?.id) {
|
||||
const owner = getLoggedInUser();
|
||||
if (!channel?.id || !community?.id || !owner?.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -192,7 +197,34 @@ const ChatView: Component = () => {
|
|||
}
|
||||
|
||||
const attachmentIds: string[] = [];
|
||||
for (const file of files) {
|
||||
|
||||
if (files.length > 0) {
|
||||
setAttachmentUploadProgress({
|
||||
total: 0,
|
||||
fileName: files[0].name,
|
||||
fileIndex: 0,
|
||||
fileCount: 0,
|
||||
fileProgress: 0,
|
||||
});
|
||||
|
||||
for (const [index, file] of files.entries()) {
|
||||
const updateProgress = (
|
||||
chunkIndex: number,
|
||||
totalChunks: number,
|
||||
) => {
|
||||
const totalProgress = index / files.length;
|
||||
const fileProgress = (chunkIndex + 1) / totalChunks;
|
||||
|
||||
setAttachmentUploadProgress({
|
||||
total:
|
||||
(totalProgress + fileProgress / files.length) * 100,
|
||||
fileName: file.name,
|
||||
fileIndex: index + 1,
|
||||
fileCount: files.length,
|
||||
fileProgress: fileProgress * 100,
|
||||
});
|
||||
};
|
||||
|
||||
const attachmentId = await createAttachment(
|
||||
community.id,
|
||||
file.name,
|
||||
|
|
@ -206,21 +238,31 @@ const ChatView: Component = () => {
|
|||
community.id,
|
||||
attachmentId,
|
||||
file,
|
||||
updateProgress,
|
||||
);
|
||||
|
||||
if (uploaded) {
|
||||
await finishAttachment(attachmentId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setAttachmentUploadProgress();
|
||||
}
|
||||
|
||||
setText(channel.id, "");
|
||||
createMessage(
|
||||
await createMessage(
|
||||
community.id,
|
||||
channel.id,
|
||||
owner.id,
|
||||
text ?? "",
|
||||
attachmentIds,
|
||||
undefined,
|
||||
);
|
||||
|
||||
attachmentIds.forEach((attachmentId) => {
|
||||
fetchMedia(community.id, attachmentId);
|
||||
});
|
||||
};
|
||||
|
||||
const onRemoveMessage = (messageId: string | undefined) => {
|
||||
|
|
@ -316,16 +358,11 @@ const ChatView: Component = () => {
|
|||
|
||||
return (
|
||||
<Message
|
||||
messageId={message.id ?? ""}
|
||||
message={message.text}
|
||||
userId={message.ownerId}
|
||||
message={message}
|
||||
username={user.nickname ?? user.username ?? ""}
|
||||
color={bestRole?.color}
|
||||
avatar={getUserAvatarUrl(user.avatar)}
|
||||
attachments={message.attachments}
|
||||
time={message.creationDate}
|
||||
plain={isPlainMessage(message, allMessages[index - 1])}
|
||||
decryptionStatus={message.decryptionStatus ?? false}
|
||||
editing={getEditingMessages().has(message.id)}
|
||||
onProfileClick={onProfileClick}
|
||||
onMessageRightClick={onMessageRightClick}
|
||||
|
|
@ -335,7 +372,9 @@ const ChatView: Component = () => {
|
|||
};
|
||||
|
||||
return (
|
||||
<div class="bg-stone-800 flex-1 z-0 relative">
|
||||
<div
|
||||
class={`${getActiveChannel() && !state.app.membersOpenMobile ? "block" : "hidden lg:block"} bg-stone-800 flex-1 z-0 relative`}
|
||||
>
|
||||
<ContextMenu
|
||||
visible={menu.getVisible()}
|
||||
position={menu.getPos()}
|
||||
|
|
@ -357,6 +396,7 @@ const ChatView: Component = () => {
|
|||
{channelInfo() ? (
|
||||
<MessageBar
|
||||
text={channelInfo()?.text ?? ""}
|
||||
uploadProgress={getAttachmentUploadProgress()}
|
||||
onChangeText={onChangeMessageText}
|
||||
onSend={onSendMessage}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -70,14 +70,14 @@ const CommunitySettingsModalView: Component<
|
|||
{getActiveCommunity()?.name} Settings
|
||||
</h3>
|
||||
<div class="divider"></div>
|
||||
<div class="flex flex-row gap-4 px-4">
|
||||
<div class="flex flex-col lg:flex-row gap-4 px-4 items-center lg:items-stretch">
|
||||
<div class="flex flex-col gap-2">
|
||||
<h3 class="text-lg font-bold text-center mb-4">
|
||||
Categories
|
||||
</h3>
|
||||
{[...pages].map(mapPageButton)}
|
||||
</div>
|
||||
<div class="divider divider-horizontal"></div>
|
||||
<div class="divider divider-vertical lg:divider-horizontal"></div>
|
||||
{getCurrentPage()}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -244,8 +244,8 @@ const CommunitySettingsChannelsPage: Component = () => {
|
|||
};
|
||||
|
||||
return (
|
||||
<div class="flex-1 flex flex-row gap-4">
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex-1 flex flex-col lg:flex-row gap-4 w-full">
|
||||
<div class="flex flex-col gap-2 items-center lg:items-stretch">
|
||||
<h3 class="text-lg font-bold text-center mb-4">Channels</h3>
|
||||
{channels().map(mapChannel)}
|
||||
{channels().length > 0 ? (
|
||||
|
|
@ -261,7 +261,7 @@ const CommunitySettingsChannelsPage: Component = () => {
|
|||
Add new Channel
|
||||
</button>
|
||||
</div>
|
||||
<div class="divider divider-horizontal"></div>
|
||||
<div class="divider divider-vertical lg:divider-horizontal"></div>
|
||||
<div class="flex-1">{optionsHtml()}</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -221,12 +221,12 @@ const CommunitySettingsMembersPage: Component = () => {
|
|||
};
|
||||
|
||||
return (
|
||||
<div class="flex-1 flex flex-row gap-4">
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex-1 flex flex-col lg:flex-row gap-4 w-full">
|
||||
<div class="flex flex-col gap-2 items-center lg:items-stretch">
|
||||
<h3 class="text-lg font-bold text-center mb-4">Members</h3>
|
||||
{members().map(mapMember)}
|
||||
</div>
|
||||
<div class="divider divider-horizontal"></div>
|
||||
<div class="divider divider-vertical lg:divider-horizontal"></div>
|
||||
<div class="flex-1">{optionsHtml()}</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -285,8 +285,8 @@ const CommunitySettingsRolesPage: Component = () => {
|
|||
};
|
||||
|
||||
return (
|
||||
<div class="flex-1 flex flex-row gap-4">
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex-1 flex flex-col lg:flex-row gap-4 w-full">
|
||||
<div class="flex flex-col gap-2 items-center lg:items-stretch">
|
||||
<h3 class="text-lg font-bold text-center mb-4">Roles</h3>
|
||||
{roles().map(mapRole)}
|
||||
{roles().length > 0 ? <div class="divider"></div> : undefined}
|
||||
|
|
@ -300,7 +300,7 @@ const CommunitySettingsRolesPage: Component = () => {
|
|||
Add new Role
|
||||
</button>
|
||||
</div>
|
||||
<div class="divider divider-horizontal"></div>
|
||||
<div class="divider divider-vertical lg:divider-horizontal"></div>
|
||||
<div class="flex-1">{optionsHtml()}</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ import {
|
|||
getCommunityAvatarUrl,
|
||||
removeCommunityMember,
|
||||
} from "../../services/community";
|
||||
import { resetActiveChannel } from "../../store/channel";
|
||||
import { getActiveChannel, resetActiveChannel } from "../../store/channel";
|
||||
import {
|
||||
ContextMenu,
|
||||
IContextMenuItem,
|
||||
|
|
@ -108,7 +108,9 @@ const CommunityView: Component = () => {
|
|||
};
|
||||
|
||||
return (
|
||||
<div class="flex flex-col bg-stone-950 w-16 h-full shadow-panel z-30 p-2 gap-2">
|
||||
<div
|
||||
class={`${getActiveChannel() ? "hidden lg:flex" : "flex"} flex flex-col bg-stone-950 w-16 h-full shadow-panel z-30 p-2 gap-2`}
|
||||
>
|
||||
<ContextMenu
|
||||
visible={menu.getVisible()}
|
||||
position={menu.getPos()}
|
||||
|
|
|
|||
|
|
@ -4,8 +4,9 @@ import { HomeCard } from "../../components/HomeCard";
|
|||
import { setAddCommunityOpen, setSettingsOpen } from "../../store/app";
|
||||
import { removeSession } from "../../services/session";
|
||||
import { useNavigate } from "@solidjs/router";
|
||||
import { state } from "../../store/state";
|
||||
import { resetState, state } from "../../store/state";
|
||||
import { resetLoggedIn, resetAuthSession } from "../../store/auth";
|
||||
import { deleteDB } from "../../services/database";
|
||||
|
||||
const HomeView: Component = () => {
|
||||
const navigate = useNavigate();
|
||||
|
|
@ -26,12 +27,15 @@ const HomeView: Component = () => {
|
|||
resetLoggedIn();
|
||||
resetAuthSession();
|
||||
|
||||
resetState();
|
||||
deleteDB();
|
||||
|
||||
navigate("/");
|
||||
};
|
||||
|
||||
return (
|
||||
<div class="flex-1 flex flex-row bg-stone-900 shadow-panel z-0">
|
||||
<div class="flex-1 flex flex-row items-center justify-center gap-8">
|
||||
<div class="bg-stone-900 flex-1 flex flex-col lg:flex-row items-center justify-center">
|
||||
<div class="flex flex-col lg:flex-row items-center justify-start gap-8 p-8 w-full lg:w-fit overflow-y-auto scrollbar-thin scrollbar-thumb-gray-500 scrollbar-track-gray-800">
|
||||
<HomeCard
|
||||
title="Find a Community"
|
||||
description="Find or create a new Community to chat"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { createEffect, createSignal, onMount, type Component } from "solid-js";
|
||||
import { state } from "../../store/state";
|
||||
import { setState, state } from "../../store/state";
|
||||
import { useNavigate } from "@solidjs/router";
|
||||
import { fetchLogin, fetchRefresh } from "../../services/auth";
|
||||
|
||||
|
|
|
|||
|
|
@ -17,11 +17,12 @@ import {
|
|||
IContextMenuItem,
|
||||
useContextMenu,
|
||||
} from "../../components/ContextMenu";
|
||||
import { DownIcon, TrashIcon, UpIcon } from "../../icons";
|
||||
import { DownIcon, LeftIcon, TrashIcon, UpIcon } from "../../icons";
|
||||
import { getActiveCommunity } from "../../store/community";
|
||||
import { IUser } from "../../store/user";
|
||||
import { IRole } from "../../store/role";
|
||||
import { getUserBestRoleId } from "../../services/role";
|
||||
import { setMembersOpenMobile } from "../../store/app";
|
||||
|
||||
const MemberView: Component = () => {
|
||||
const roleMenu = useContextMenu<string>("channel-role-menu");
|
||||
|
|
@ -243,7 +244,9 @@ const MemberView: Component = () => {
|
|||
};
|
||||
|
||||
return (
|
||||
<div class="bg-stone-900 w-64 shadow-panel z-20 relative">
|
||||
<div
|
||||
class={`${state.app.membersOpenMobile ? "block w-full" : "hidden "} lg:block bg-stone-900 lg:w-64 shadow-panel z-20 relative`}
|
||||
>
|
||||
<ContextMenu
|
||||
visible={roleMenu.getVisible()}
|
||||
position={roleMenu.getPos()}
|
||||
|
|
@ -257,7 +260,13 @@ const MemberView: Component = () => {
|
|||
items={memberMenuItems}
|
||||
/>
|
||||
<div class="h-full">
|
||||
<ul class="h-full list flex flex-col p-2 gap-1 overflow-y-auto scrollbar-thin scrollbar-thumb-gray-500 scrollbar-track-gray-800">
|
||||
<div
|
||||
class="block lg:hidden ml-1 w-16 p-5 cursor-pointer"
|
||||
onClick={() => setMembersOpenMobile(false)}
|
||||
>
|
||||
<LeftIcon />
|
||||
</div>
|
||||
<ul class="list flex flex-col p-2 gap-1 overflow-y-auto scrollbar-thin scrollbar-thumb-gray-500 scrollbar-track-gray-800">
|
||||
{roles().map(mapRole)}
|
||||
</ul>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -47,14 +47,14 @@ const SettingsModalView: Component<ISettingsModalViewProps> = (
|
|||
<div class="modal-box w-10/12 max-w-5xl h-11/12 2xl:h-9/12 bg-stone-950 rounded-3xl">
|
||||
<h3 class="text-lg font-bold text-center">Settings</h3>
|
||||
<div class="divider"></div>
|
||||
<div class="flex flex-row gap-4 px-4">
|
||||
<div class="flex flex-col lg:flex-row gap-4 px-4 items-center lg:items-stretch">
|
||||
<div class="flex flex-col gap-2">
|
||||
<h3 class="text-lg font-bold text-center mb-4">
|
||||
Categories
|
||||
</h3>
|
||||
{[...pages].map(mapPageButton)}
|
||||
</div>
|
||||
<div class="divider divider-horizontal"></div>
|
||||
<div class="divider divider-vertical lg:divider-horizontal"></div>
|
||||
{getCurrentPage()}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -146,12 +146,12 @@ const SettingsCommunitiesPage: Component = () => {
|
|||
};
|
||||
|
||||
return (
|
||||
<div class="flex-1 flex flex-row gap-4">
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex-1 flex flex-col lg:flex-row gap-4 w-full">
|
||||
<div class="flex flex-col gap-2 items-center lg:items-stretch">
|
||||
<h3 class="text-lg font-bold text-center mb-4">Communities</h3>
|
||||
{communityIds().map(mapCommunity)}
|
||||
</div>
|
||||
<div class="divider divider-horizontal"></div>
|
||||
<div class="divider divider-vertical lg:divider-horizontal"></div>
|
||||
<div class="flex-1">{optionsHtml()}</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -6,11 +6,12 @@ import {
|
|||
type Component,
|
||||
} from "solid-js";
|
||||
import { fetchUserSessions } from "../../../../services/user";
|
||||
import { state } from "../../../../store/state";
|
||||
import { resetState, state } from "../../../../store/state";
|
||||
import { RichSettingsItem } from "../../../../components/RichSettingsItem";
|
||||
import { DeviceIcon, MinusIcon, TrashIcon } from "../../../../icons";
|
||||
import { removeSession } from "../../../../services/session";
|
||||
import { useNavigate } from "@solidjs/router";
|
||||
import { deleteDB } from "../../../../services/database";
|
||||
|
||||
const SettingsSessionPage: Component = () => {
|
||||
const [getSelectedSessionId, setSelectedSessionId] = createSignal<
|
||||
|
|
@ -43,6 +44,9 @@ const SettingsSessionPage: Component = () => {
|
|||
removeSession(sessionId);
|
||||
|
||||
if (sessionId === state.auth.session?.id) {
|
||||
resetState();
|
||||
deleteDB();
|
||||
|
||||
navigate("/");
|
||||
}
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue