message date spacer

This commit is contained in:
Michael Peters 2022-02-10 17:36:16 -06:00
parent 43814a0b6e
commit 73e81f0bc9
5 changed files with 185 additions and 35 deletions

View File

@ -94,7 +94,13 @@ export class Member implements WithEquals<Member> {
}
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 {
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) {
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 member = this.member instanceof Member ? this.member.displayName : `m#${this.member.id}`;
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 {
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
// messages sent in quick succession (within 5 minutes) of the previous message
// by the same sender are considered continued messages.
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 {
@ -236,7 +262,10 @@ export class Message implements WithEquals<Message> {
isImageResource(): boolean {
return !!(
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 {
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> {
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) {
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 {
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);
}
@ -300,7 +347,14 @@ export class Resource implements WithEquals<Resource> {
}
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>) {
if (this.member) {
@ -312,11 +366,24 @@ export class Token implements WithEquals<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) {
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) {

View File

@ -1,5 +1,24 @@
@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 {
display: flex;
padding: 4px 16px;

View File

@ -5,7 +5,12 @@ import { Member, Message } from '../../../data-types';
import CombinedGuild from '../../../guild-combined';
import ImageContextMenu from '../../contexts/context-menu-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 { isLoaded } from '../../require/loadables';
import { useContextClickContextMenu, useDownloadButton, useOneTimeAsyncAction } from '../../require/react-helper';
@ -19,14 +24,19 @@ interface ResourceElementProps {
const ResourceElement: FC<ResourceElementProps> = (props: ResourceElementProps) => {
const { guild, resourceId, resourceName } = props;
const [callable, text, shaking] = useDownloadButton(resourceName, async () => (await guild.fetchResource(resourceId)).data, [guild, resourceId], {
start: 'Click to Download',
pendingFetch: 'Fetching...',
errorFetch: 'Unable to Download. Try Again',
pendingSave: 'Saving...',
errorSave: 'Unable to Save. Try Again',
success: 'Click to Open in Explorer',
});
const [callable, text, shaking] = useDownloadButton(
resourceName,
async () => (await guild.fetchResource(resourceId)).data,
[guild, resourceId],
{
start: 'Click to Download',
pendingFetch: 'Fetching...',
errorFetch: 'Unable to Download. Try Again',
pendingSave: 'Saving...',
errorSave: 'Unable to Save. Try Again',
success: 'Click to Open in Explorer',
},
);
return (
<div className="content resource" onClick={callable}>
@ -55,12 +65,22 @@ const PreviewImageElement: FC<PreviewImageElementProps> = (props: PreviewImageEl
// TODO: Handle resourceError
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(
(_alignment: IAlignment, relativeToPos: { x: number; y: number }, close: () => void) => {
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],
);
@ -86,7 +106,10 @@ export interface MessageElementProps {
const MessageElement: FC<MessageElementProps> = (props: MessageElementProps) => {
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(
async () => {
@ -98,6 +121,17 @@ const MessageElement: FC<MessageElementProps> = (props: MessageElementProps) =>
[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(() => {
if (message.isContinued(prevMessage)) {
return <div className="timestamp">{moment(message.sent).format('HH:mm')}</div>;
@ -152,19 +186,35 @@ const MessageElement: FC<MessageElementProps> = (props: MessageElementProps) =>
/>
);
} 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 (
<div className={className}>
{leftPart}
<div className="right">
{header}
{resourceElement}
<div className="content text">{ElementsUtil.parseMessageText(message.text ?? '')}</div>
<>
{dateSpacer}
<div className={className}>
{leftPart}
<div className="right">
{header}
{resourceElement}
<div className="content text">{ElementsUtil.parseMessageText(message.text ?? '')}</div>
</div>
</div>
</div>
</>
);
};

View File

@ -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
// there is no way to cancel this function
// 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);
await Util.sleep(delayMs);
setState(_old => !start);
@ -119,7 +123,11 @@ export default class ElementsUtil {
const obj: SimpleQElement = { tag: 'span', content: [], class: null };
const stack: SimpleQElement[] = [obj];
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
return {
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
// relativeTo can be an { x, y } or HTMLElement
/** 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) {
const added = name + ' +';
const subbed = name + ' -';

View File

@ -39,6 +39,8 @@ $background-dropdown-option-hover: #1a1c1f;
$channels-default: #8e9297;
$channeltextarea-background: #40444b;
$background-date-spacer: #41454c;
$brand: #7289ba; /* yea that's a direct copy from discord */
$brand-hover: #677bc4; /* thicc */
$error: #d71f28; /* In fact, not copied! */