message date spacer
This commit is contained in:
parent
43814a0b6e
commit
73e81f0bc9
@ -94,7 +94,13 @@ export class Member implements WithEquals<Member> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class Channel implements WithEquals<Channel> {
|
export class Channel implements WithEquals<Channel> {
|
||||||
private constructor(public readonly id: string, public readonly index: number, public readonly name: string, public readonly flavorText: string | null, public readonly source?: unknown) {}
|
private constructor(
|
||||||
|
public readonly id: string,
|
||||||
|
public readonly index: number,
|
||||||
|
public readonly name: string,
|
||||||
|
public readonly flavorText: string | null,
|
||||||
|
public readonly source?: unknown,
|
||||||
|
) {}
|
||||||
|
|
||||||
static fromDBData(dataChannel: any): Channel {
|
static fromDBData(dataChannel: any): Channel {
|
||||||
return new Channel(dataChannel.id, dataChannel.index, dataChannel.name, dataChannel.flavor_text, dataChannel);
|
return new Channel(dataChannel.id, dataChannel.index, dataChannel.name, dataChannel.flavor_text, dataChannel);
|
||||||
@ -109,7 +115,12 @@ export class Channel implements WithEquals<Channel> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
equals(other: Channel) {
|
equals(other: Channel) {
|
||||||
return this.id === other.id && this.index === other.index && this.name === other.name && this.flavorText === other.flavorText;
|
return (
|
||||||
|
this.id === other.id &&
|
||||||
|
this.index === other.index &&
|
||||||
|
this.name === other.name &&
|
||||||
|
this.flavorText === other.flavorText
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -190,7 +201,9 @@ export class Message implements WithEquals<Message> {
|
|||||||
const channel = this.channel instanceof Channel ? this.channel.name : `ch#${this.channel.id}`;
|
const channel = this.channel instanceof Channel ? this.channel.name : `ch#${this.channel.id}`;
|
||||||
const member = this.member instanceof Member ? this.member.displayName : `m#${this.member.id}`;
|
const member = this.member instanceof Member ? this.member.displayName : `m#${this.member.id}`;
|
||||||
if (this.resourceName) {
|
if (this.resourceName) {
|
||||||
return `msg#${this.id} ${channel} @${formatDate(this.sent)} ${member}: ${this.text ? this.text + ' / ' : ''}${this.resourceName}`;
|
return `msg#${this.id} ${channel} @${formatDate(this.sent)} ${member}: ${
|
||||||
|
this.text ? this.text + ' / ' : ''
|
||||||
|
}${this.resourceName}`;
|
||||||
} else {
|
} else {
|
||||||
return `msg#${this.id} ${channel} @${formatDate(this.sent)} ${member}: ${this.text}`;
|
return `msg#${this.id} ${channel} @${formatDate(this.sent)} ${member}: ${this.text}`;
|
||||||
}
|
}
|
||||||
@ -222,11 +235,24 @@ export class Message implements WithEquals<Message> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// returns true if the message happens on the next day from the last message
|
||||||
|
isNextDay(lastMessage: Message | null): boolean {
|
||||||
|
if (!lastMessage) return true; // this is the first message
|
||||||
|
const msPerDay = 24 * 60 * 60 * 1000;
|
||||||
|
const lastMessageDay = Math.floor(lastMessage.sent.getTime() / msPerDay);
|
||||||
|
const messageDay = Math.floor(this.sent.getTime() / msPerDay);
|
||||||
|
return messageDay !== lastMessageDay;
|
||||||
|
}
|
||||||
|
|
||||||
// returns true if this message should be considered 'continued' from the last message
|
// returns true if this message should be considered 'continued' from the last message
|
||||||
// messages sent in quick succession (within 5 minutes) of the previous message
|
// messages sent in quick succession (within 5 minutes) of the previous message
|
||||||
// by the same sender are considered continued messages.
|
// by the same sender are considered continued messages.
|
||||||
isContinued(lastMessage: Message | null): boolean {
|
isContinued(lastMessage: Message | null): boolean {
|
||||||
return !!(lastMessage && lastMessage.member.id === this.member.id && this.sent.getTime() - lastMessage.sent.getTime() < 5 * 60 * 1000);
|
return !!(
|
||||||
|
lastMessage &&
|
||||||
|
lastMessage.member.id === this.member.id &&
|
||||||
|
this.sent.getTime() - lastMessage.sent.getTime() < 5 * 60 * 1000
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
hasResource(): boolean {
|
hasResource(): boolean {
|
||||||
@ -236,7 +262,10 @@ export class Message implements WithEquals<Message> {
|
|||||||
isImageResource(): boolean {
|
isImageResource(): boolean {
|
||||||
return !!(
|
return !!(
|
||||||
this.hasResource() &&
|
this.hasResource() &&
|
||||||
(this.resourceName?.toLowerCase().endsWith('.png') || this.resourceName?.toLowerCase().endsWith('.jpg') || this.resourceName?.toLowerCase().endsWith('.jpeg') || this.resourceName?.toLowerCase().endsWith('.gif'))
|
(this.resourceName?.toLowerCase().endsWith('.png') ||
|
||||||
|
this.resourceName?.toLowerCase().endsWith('.jpg') ||
|
||||||
|
this.resourceName?.toLowerCase().endsWith('.jpeg') ||
|
||||||
|
this.resourceName?.toLowerCase().endsWith('.gif'))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -253,12 +282,24 @@ export class SocketConfig {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
static fromDBData(data: any): SocketConfig {
|
static fromDBData(data: any): SocketConfig {
|
||||||
return new SocketConfig(data.id, data.guild_id, data.url, data.cert, crypto.createPublicKey(data.public_key), crypto.createPrivateKey(data.private_key), data);
|
return new SocketConfig(
|
||||||
|
data.id,
|
||||||
|
data.guild_id,
|
||||||
|
data.url,
|
||||||
|
data.cert,
|
||||||
|
crypto.createPublicKey(data.public_key),
|
||||||
|
crypto.createPrivateKey(data.private_key),
|
||||||
|
data,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class GuildMetadata implements WithEquals<GuildMetadata> {
|
export class GuildMetadata implements WithEquals<GuildMetadata> {
|
||||||
public constructor(public readonly name: string, public readonly iconResourceId: string, public readonly source?: unknown) {}
|
public constructor(
|
||||||
|
public readonly name: string,
|
||||||
|
public readonly iconResourceId: string,
|
||||||
|
public readonly source?: unknown,
|
||||||
|
) {}
|
||||||
|
|
||||||
static fromDBData(data: any) {
|
static fromDBData(data: any) {
|
||||||
return new GuildMetadata(data.name, data.icon_resource_id, data);
|
return new GuildMetadata(data.name, data.icon_resource_id, data);
|
||||||
@ -274,7 +315,13 @@ export class GuildMetadata implements WithEquals<GuildMetadata> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class GuildMetadataLocal extends GuildMetadata {
|
export class GuildMetadataLocal extends GuildMetadata {
|
||||||
private constructor(public readonly id: number, name: string, iconResourceId: string, public readonly memberId: string | null, source?: unknown) {
|
private constructor(
|
||||||
|
public readonly id: number,
|
||||||
|
name: string,
|
||||||
|
iconResourceId: string,
|
||||||
|
public readonly memberId: string | null,
|
||||||
|
source?: unknown,
|
||||||
|
) {
|
||||||
super(name, iconResourceId, source);
|
super(name, iconResourceId, source);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -300,7 +347,14 @@ export class Resource implements WithEquals<Resource> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class Token implements WithEquals<Token> {
|
export class Token implements WithEquals<Token> {
|
||||||
constructor(public readonly id: string, public readonly token: string, public member: Member | { id: string } | null, public readonly created: Date, public readonly expires: Date | null, public readonly source?: unknown) {}
|
constructor(
|
||||||
|
public readonly id: string,
|
||||||
|
public readonly token: string,
|
||||||
|
public member: Member | { id: string } | null,
|
||||||
|
public readonly created: Date,
|
||||||
|
public readonly expires: Date | null,
|
||||||
|
public readonly source?: unknown,
|
||||||
|
) {}
|
||||||
|
|
||||||
fill(members: Map<string, Member>) {
|
fill(members: Map<string, Member>) {
|
||||||
if (this.member) {
|
if (this.member) {
|
||||||
@ -312,11 +366,24 @@ export class Token implements WithEquals<Token> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static fromDBData(dataToken: any): Token {
|
static fromDBData(dataToken: any): Token {
|
||||||
return new Token(dataToken.id, dataToken.token, dataToken.member_id ? { id: dataToken.member_id } : null, new Date(dataToken.created), dataToken.expires ? new Date(dataToken.expires) : null, dataToken);
|
return new Token(
|
||||||
|
dataToken.id,
|
||||||
|
dataToken.token,
|
||||||
|
dataToken.member_id ? { id: dataToken.member_id } : null,
|
||||||
|
new Date(dataToken.created),
|
||||||
|
dataToken.expires ? new Date(dataToken.expires) : null,
|
||||||
|
dataToken,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
equals(other: Token) {
|
equals(other: Token) {
|
||||||
return this.id === other.id && this.token === other.token && this.member?.id === other.member?.id && this.created === other.created && this.expires === other.expires;
|
return (
|
||||||
|
this.id === other.id &&
|
||||||
|
this.token === other.token &&
|
||||||
|
this.member?.id === other.member?.id &&
|
||||||
|
this.created === other.created &&
|
||||||
|
this.expires === other.expires
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static sortFurthestExpiresFirst(a: Token, b: Token) {
|
static sortFurthestExpiresFirst(a: Token, b: Token) {
|
||||||
|
@ -1,5 +1,24 @@
|
|||||||
@use '../../../styles/theme.scss';
|
@use '../../../styles/theme.scss';
|
||||||
|
|
||||||
|
.date-spacer {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin: 8px 16px;
|
||||||
|
|
||||||
|
.horizontal-line {
|
||||||
|
flex: 1;
|
||||||
|
height: 1px;
|
||||||
|
background-color: theme.$background-date-spacer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date {
|
||||||
|
font-size: 0.7em;
|
||||||
|
font-weight: 600;
|
||||||
|
color: theme.$text-muted;
|
||||||
|
margin: 0 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.message-react {
|
.message-react {
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: 4px 16px;
|
padding: 4px 16px;
|
||||||
|
@ -5,7 +5,12 @@ import { Member, Message } from '../../../data-types';
|
|||||||
import CombinedGuild from '../../../guild-combined';
|
import CombinedGuild from '../../../guild-combined';
|
||||||
import ImageContextMenu from '../../contexts/context-menu-image';
|
import ImageContextMenu from '../../contexts/context-menu-image';
|
||||||
import ImageOverlay from '../../overlays/overlay-image';
|
import ImageOverlay from '../../overlays/overlay-image';
|
||||||
import { guildResourceSoftImgSrcState, guildResourceState, overlayState, useRecoilValueSoftImgSrc } from '../../require/atoms';
|
import {
|
||||||
|
guildResourceSoftImgSrcState,
|
||||||
|
guildResourceState,
|
||||||
|
overlayState,
|
||||||
|
useRecoilValueSoftImgSrc,
|
||||||
|
} from '../../require/atoms';
|
||||||
import ElementsUtil, { IAlignment } from '../../require/elements-util';
|
import ElementsUtil, { IAlignment } from '../../require/elements-util';
|
||||||
import { isLoaded } from '../../require/loadables';
|
import { isLoaded } from '../../require/loadables';
|
||||||
import { useContextClickContextMenu, useDownloadButton, useOneTimeAsyncAction } from '../../require/react-helper';
|
import { useContextClickContextMenu, useDownloadButton, useOneTimeAsyncAction } from '../../require/react-helper';
|
||||||
@ -19,14 +24,19 @@ interface ResourceElementProps {
|
|||||||
const ResourceElement: FC<ResourceElementProps> = (props: ResourceElementProps) => {
|
const ResourceElement: FC<ResourceElementProps> = (props: ResourceElementProps) => {
|
||||||
const { guild, resourceId, resourceName } = props;
|
const { guild, resourceId, resourceName } = props;
|
||||||
|
|
||||||
const [callable, text, shaking] = useDownloadButton(resourceName, async () => (await guild.fetchResource(resourceId)).data, [guild, resourceId], {
|
const [callable, text, shaking] = useDownloadButton(
|
||||||
start: 'Click to Download',
|
resourceName,
|
||||||
pendingFetch: 'Fetching...',
|
async () => (await guild.fetchResource(resourceId)).data,
|
||||||
errorFetch: 'Unable to Download. Try Again',
|
[guild, resourceId],
|
||||||
pendingSave: 'Saving...',
|
{
|
||||||
errorSave: 'Unable to Save. Try Again',
|
start: 'Click to Download',
|
||||||
success: 'Click to Open in Explorer',
|
pendingFetch: 'Fetching...',
|
||||||
});
|
errorFetch: 'Unable to Download. Try Again',
|
||||||
|
pendingSave: 'Saving...',
|
||||||
|
errorSave: 'Unable to Save. Try Again',
|
||||||
|
success: 'Click to Open in Explorer',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="content resource" onClick={callable}>
|
<div className="content resource" onClick={callable}>
|
||||||
@ -55,12 +65,22 @@ const PreviewImageElement: FC<PreviewImageElementProps> = (props: PreviewImageEl
|
|||||||
|
|
||||||
// TODO: Handle resourceError
|
// TODO: Handle resourceError
|
||||||
const previewResource = useRecoilValue(guildResourceState({ guildId: guild.id, resourceId: resourcePreviewId }));
|
const previewResource = useRecoilValue(guildResourceState({ guildId: guild.id, resourceId: resourcePreviewId }));
|
||||||
const previewImgSrc = useRecoilValueSoftImgSrc(guildResourceSoftImgSrcState({ guildId: guild.id, resourceId: resourcePreviewId }));
|
const previewImgSrc = useRecoilValueSoftImgSrc(
|
||||||
|
guildResourceSoftImgSrcState({ guildId: guild.id, resourceId: resourcePreviewId }),
|
||||||
|
);
|
||||||
|
|
||||||
const [contextMenu, onContextMenu] = useContextClickContextMenu(
|
const [contextMenu, onContextMenu] = useContextClickContextMenu(
|
||||||
(_alignment: IAlignment, relativeToPos: { x: number; y: number }, close: () => void) => {
|
(_alignment: IAlignment, relativeToPos: { x: number; y: number }, close: () => void) => {
|
||||||
if (!isLoaded(previewResource)) return null;
|
if (!isLoaded(previewResource)) return null;
|
||||||
return <ImageContextMenu relativeToPos={relativeToPos} close={close} resourceName={resourceName} resourceBuff={previewResource.value.data} isPreview={true} />;
|
return (
|
||||||
|
<ImageContextMenu
|
||||||
|
relativeToPos={relativeToPos}
|
||||||
|
close={close}
|
||||||
|
resourceName={resourceName}
|
||||||
|
resourceBuff={previewResource.value.data}
|
||||||
|
isPreview={true}
|
||||||
|
/>
|
||||||
|
);
|
||||||
},
|
},
|
||||||
[previewResource, resourceName],
|
[previewResource, resourceName],
|
||||||
);
|
);
|
||||||
@ -86,7 +106,10 @@ export interface MessageElementProps {
|
|||||||
const MessageElement: FC<MessageElementProps> = (props: MessageElementProps) => {
|
const MessageElement: FC<MessageElementProps> = (props: MessageElementProps) => {
|
||||||
const { guild, message, prevMessage } = props;
|
const { guild, message, prevMessage } = props;
|
||||||
|
|
||||||
const className = useMemo(() => (message.isContinued(prevMessage) ? 'message-react continued' : 'message-react'), [message, prevMessage]);
|
const className = useMemo(
|
||||||
|
() => (message.isContinued(prevMessage) ? 'message-react continued' : 'message-react'),
|
||||||
|
[message, prevMessage],
|
||||||
|
);
|
||||||
|
|
||||||
const [avatarSrc] = useOneTimeAsyncAction(
|
const [avatarSrc] = useOneTimeAsyncAction(
|
||||||
async () => {
|
async () => {
|
||||||
@ -98,6 +121,17 @@ const MessageElement: FC<MessageElementProps> = (props: MessageElementProps) =>
|
|||||||
[message],
|
[message],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const dateSpacer = useMemo(() => {
|
||||||
|
if (message.isContinued(prevMessage)) return null;
|
||||||
|
return (
|
||||||
|
<div className="date-spacer">
|
||||||
|
<div className="horizontal-line" />
|
||||||
|
<div className="date">{moment(message.sent).format('MMMM DD, YYYY')}</div>
|
||||||
|
<div className="horizontal-line" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}, [message, prevMessage]);
|
||||||
|
|
||||||
const leftPart = useMemo(() => {
|
const leftPart = useMemo(() => {
|
||||||
if (message.isContinued(prevMessage)) {
|
if (message.isContinued(prevMessage)) {
|
||||||
return <div className="timestamp">{moment(message.sent).format('HH:mm')}</div>;
|
return <div className="timestamp">{moment(message.sent).format('HH:mm')}</div>;
|
||||||
@ -152,19 +186,35 @@ const MessageElement: FC<MessageElementProps> = (props: MessageElementProps) =>
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return <ResourceElement guild={guild} resourceId={message.resourceId} resourceName={message.resourceName ?? 'unknown.unk'} />;
|
return (
|
||||||
|
<ResourceElement
|
||||||
|
guild={guild}
|
||||||
|
resourceId={message.resourceId}
|
||||||
|
resourceName={message.resourceName ?? 'unknown.unk'}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}, [guild, message.previewHeight, message.previewWidth, message.resourceId, message.resourceName, message.resourcePreviewId]);
|
}, [
|
||||||
|
guild,
|
||||||
|
message.previewHeight,
|
||||||
|
message.previewWidth,
|
||||||
|
message.resourceId,
|
||||||
|
message.resourceName,
|
||||||
|
message.resourcePreviewId,
|
||||||
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={className}>
|
<>
|
||||||
{leftPart}
|
{dateSpacer}
|
||||||
<div className="right">
|
<div className={className}>
|
||||||
{header}
|
{leftPart}
|
||||||
{resourceElement}
|
<div className="right">
|
||||||
<div className="content text">{ElementsUtil.parseMessageText(message.text ?? '')}</div>
|
{header}
|
||||||
|
{resourceElement}
|
||||||
|
<div className="content text">{ElementsUtil.parseMessageText(message.text ?? '')}</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -50,7 +50,11 @@ export default class ElementsUtil {
|
|||||||
// calls a function with the start parameter and then the inverse of the start parameter after a determined number of ms
|
// calls a function with the start parameter and then the inverse of the start parameter after a determined number of ms
|
||||||
// there is no way to cancel this function
|
// there is no way to cancel this function
|
||||||
// useful for enabling a "shake" element for a pre-determined amount of time
|
// useful for enabling a "shake" element for a pre-determined amount of time
|
||||||
static async delayToggleState(setState: React.Dispatch<React.SetStateAction<boolean>>, delayMs: number, start = true): Promise<void> {
|
static async delayToggleState(
|
||||||
|
setState: React.Dispatch<React.SetStateAction<boolean>>,
|
||||||
|
delayMs: number,
|
||||||
|
start = true,
|
||||||
|
): Promise<void> {
|
||||||
setState(start);
|
setState(start);
|
||||||
await Util.sleep(delayMs);
|
await Util.sleep(delayMs);
|
||||||
setState(_old => !start);
|
setState(_old => !start);
|
||||||
@ -119,7 +123,11 @@ export default class ElementsUtil {
|
|||||||
const obj: SimpleQElement = { tag: 'span', content: [], class: null };
|
const obj: SimpleQElement = { tag: 'span', content: [], class: null };
|
||||||
const stack: SimpleQElement[] = [obj];
|
const stack: SimpleQElement[] = [obj];
|
||||||
let idx = 0;
|
let idx = 0;
|
||||||
function makeEscape(regex: RegExp, len: number, str: string): { matcher: RegExp; response: (i: number) => void } {
|
function makeEscape(
|
||||||
|
regex: RegExp,
|
||||||
|
len: number,
|
||||||
|
str: string,
|
||||||
|
): { matcher: RegExp; response: (i: number) => void } {
|
||||||
// function for readability
|
// function for readability
|
||||||
return {
|
return {
|
||||||
matcher: regex,
|
matcher: regex,
|
||||||
@ -203,7 +211,11 @@ export default class ElementsUtil {
|
|||||||
// and align the left of the element to the right of the relative to plus 20 pixels
|
// and align the left of the element to the right of the relative to plus 20 pixels
|
||||||
// relativeTo can be an { x, y } or HTMLElement
|
// relativeTo can be an { x, y } or HTMLElement
|
||||||
/** aligns an element relative to another element or a position in the window */
|
/** aligns an element relative to another element or a position in the window */
|
||||||
static alignContextElement(element: HTMLElement, relativeTo: HTMLElement | { x: number; y: number }, alignment: IAlignment): void {
|
static alignContextElement(
|
||||||
|
element: HTMLElement,
|
||||||
|
relativeTo: HTMLElement | { x: number; y: number },
|
||||||
|
alignment: IAlignment,
|
||||||
|
): void {
|
||||||
function getOffset(alignment: string, name: string) {
|
function getOffset(alignment: string, name: string) {
|
||||||
const added = name + ' +';
|
const added = name + ' +';
|
||||||
const subbed = name + ' -';
|
const subbed = name + ' -';
|
||||||
|
@ -39,6 +39,8 @@ $background-dropdown-option-hover: #1a1c1f;
|
|||||||
$channels-default: #8e9297;
|
$channels-default: #8e9297;
|
||||||
$channeltextarea-background: #40444b;
|
$channeltextarea-background: #40444b;
|
||||||
|
|
||||||
|
$background-date-spacer: #41454c;
|
||||||
|
|
||||||
$brand: #7289ba; /* yea that's a direct copy from discord */
|
$brand: #7289ba; /* yea that's a direct copy from discord */
|
||||||
$brand-hover: #677bc4; /* thicc */
|
$brand-hover: #677bc4; /* thicc */
|
||||||
$error: #d71f28; /* In fact, not copied! */
|
$error: #d71f28; /* In fact, not copied! */
|
||||||
|
Loading…
Reference in New Issue
Block a user