0.5.0-pre-release

This commit is contained in:
ADAMJR 2023-01-05 17:37:41 +00:00
parent d47476d5b9
commit 5ae54305f4
19 changed files with 98 additions and 72 deletions

View File

@ -8,13 +8,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Added
- Mentions: Mention channels, and users which selectively highlights associated users/channels.
![](https://i.ibb.co/ZM4SNLw/image.png)
### Fixed
- Message formatting does not work.
- Message dates no longer show 2 days ago as 'Yesterday'.
### Changed
### Removed
## [Winter 0.4.2-alpha] - 2023/01/03
### Added

View File

@ -42,10 +42,15 @@ export default class implements WSEvent<'MESSAGE_CREATE'> {
private filterContent(content: string | undefined, filterProfanity: boolean) {
const badWords = new ProfanityFilter({ placeHolder: '?' });
// server filters tags, client renders them as is
const innerMentionPattern = /.\d{18}/gm;
const allowedTags = content?.match(innerMentionPattern) ?? [];
if (content && filterProfanity)
return striptags(badWords.clean(content));
return badWords.clean(content);
else if (content)
return striptags(content);
return content;
return '';
}
}

View File

@ -4,5 +4,5 @@ REACT_APP_CDN_URL="http://localhost:3000/assets"
REACT_APP_WEBSITE_URL="http://localhost:4200"
REACT_APP_REPO_URL="https://github.com/acrdapp/app"
REACT_APP_VERSION_NAME="Winter"
REACT_APP_VERSION_NUMBER="0.4.2-alpha"
REACT_APP_VERSION_NUMBER="0.5.0-pre-release"
REACT_APP_ROOT_API_URL="http://localhost:3000"

View File

@ -4,5 +4,5 @@ REACT_APP_CDN_URL="https://api.acrd.app/assets"
REACT_APP_WEBSITE_URL="https://acrd.app"
REACT_APP_REPO_URL="https://github.com/acrdapp/app"
REACT_APP_VERSION_NAME="Winter"
REACT_APP_VERSION_NUMBER="0.4.2-alpha"
REACT_APP_VERSION_NUMBER="0.5.0-pre-release"
REACT_APP_ROOT_API_URL="https://api.acrd.app"

View File

@ -1,6 +1,8 @@
import classNames from 'classnames';
import { useRef } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import useFormat from '../../../hooks/use-format';
import useMentions from '../../../hooks/use-mentions';
import usePerms from '../../../hooks/use-perms';
import { startTyping } from '../../../store/typing';
import { actions as ui } from '../../../store/ui';
@ -9,9 +11,10 @@ interface MessageBoxInputProps {
contentState: [any, any];
saveEdit: () => any;
}
const MessageBoxInput: React.FunctionComponent<MessageBoxInputProps> = (props) => {
const channel = useSelector((s: Store.AppState) => s.ui.activeChannel)!;
const mentions = useMentions();
const dispatch = useDispatch();
const editingMessageId = useSelector((s: Store.AppState) => s.ui.editingMessageId);
const guild = useSelector((s: Store.AppState) => s.ui.activeGuild)!;
@ -19,12 +22,12 @@ const MessageBoxInput: React.FunctionComponent<MessageBoxInputProps> = (props) =
const messageBoxRef = useRef<HTMLDivElement>(null);
const [content, setContent] = props.contentState;
const onKeyUp = (event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.key === 'Enter' && !event.shiftKey)
const onKeyUp = (event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.key === 'Enter' && !event.shiftKey)
event.preventDefault();
const text = event.currentTarget!.innerText.trim();
setContent(text);
const text = event.currentTarget!.innerHTML.trim();
setContent(mentions.formatOriginal(text));
handleEscape(event);
dispatch(startTyping(channel.id));
@ -33,19 +36,18 @@ const MessageBoxInput: React.FunctionComponent<MessageBoxInputProps> = (props) =
if (event.key !== 'Enter'
|| event.shiftKey
|| !emptyMessage) return;
props.saveEdit();
setContent('');
messageBoxRef.current!.innerText = '';
}
const handleEscape = (event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.key !== 'Escape')
return ;
if (event.key !== 'Escape') return;
if (editingMessageId)
dispatch(ui.stoppedEditingMessage());
}
const canSend = perms.canInChannel('SEND_MESSAGES', guild.id, channel.id);
const getPlaceholder = (): string | undefined => {
if (!canSend) return `Insufficient perms.`;

View File

@ -3,6 +3,7 @@ import React from 'react';
import { useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { Link } from 'react-router-dom';
import useMentions from '../../../hooks/use-mentions';
import { createMessage, updateMessage } from '../../../store/messages';
import { actions as ui } from '../../../store/ui';
import MessageBoxInput from './message-box-input';
@ -20,6 +21,7 @@ const MessageBox: React.FunctionComponent<MessageBoxProps> = (props) => {
const [content, setContent] = useState(props.content ?? '');
const channel = useSelector((s: Store.AppState) => s.ui.activeChannel)!;
const editingMessageId = useSelector((s: Store.AppState) => s.ui.editingMessageId);
const mentions = useMentions();
useEffect(() => {
const messageBox = document.querySelector('#messageBox') as HTMLDivElement;
@ -53,7 +55,7 @@ const MessageBox: React.FunctionComponent<MessageBoxProps> = (props) => {
contentState={[content, setContent]}
saveEdit={saveEdit} />
</div>
<div className="text-sm w-full h-6">
<div className="text-sm h-6 w-full">
{(editingMessageId)
? <span className="text-xs py-2">
escape to <Link to="#" onClick={stopEditing}>cancel</Link>

View File

@ -5,6 +5,7 @@ import { useDispatch, useSelector } from 'react-redux';
import { previewImage } from '../../../store/ui';
import useFormat from '../../../hooks/use-format';
import striptags from 'striptags';
import { patterns } from '../../../types/lib/patterns.types';
interface MessageContentProps {
message: Entity.Message;
@ -16,15 +17,12 @@ const MessageContent: FunctionComponent<MessageContentProps> = ({ message }) =>
const editingMessageId = useSelector((s: Store.AppState) => s.ui.editingMessageId);
const messageHTML =
((message.content)
? format(striptags(message.content))
: ''
) + ((message.updatedAt && message.content)
((message.content) ? format(message.content) : '') +
((message.updatedAt && message.content)
? `<span
class="select-none muted edited text-xs ml-1"
title="${message.updatedAt}">(edited)</span>`
: ''
);
: '');
const Attachments: React.FunctionComponent = () => (
<>

View File

@ -14,9 +14,8 @@ const MessageTimestamp: FunctionComponent<MessageTimestampProps> = ({ message })
const daysAgo = Math.floor(toDays(midnight) - toDays(createdAt));
function getTimestamp() {
const wasToday = midnight.getDate() === createdAt.getDate();
if (wasToday) return '[Today at] HH:mm';
else if (daysAgo <= 1) return '[Yesterday at] HH:mm';
if (daysAgo === -1) return '[Today at] HH:mm';
else if (daysAgo === 0) return '[Yesterday at] HH:mm';
return 'DD/MM/YYYY';
}
return <span>{moment(createdAt).format(getTimestamp())}</span>;

View File

@ -6,7 +6,7 @@ import { useDispatch, useSelector } from 'react-redux';
import usePerms from '../../../hooks/use-perms';
import { getMember, kickMember } from '../../../store/members';
import { actions as ui, openUserProfile } from '../../../store/ui';
import { toggleBlockUser } from '../../../store/users';
import { getTag, toggleBlockUser } from '../../../store/users';
import Category from '../../utils/category';
import DevModeMenuSection from '../dev-mode-menu-section';
import RoleManager from './role-manager';
@ -40,6 +40,9 @@ const GuildMemberMenu: React.FunctionComponent<GuildMemberMenuProps> = ({ user }
<ContextMenu
id={user.id}
className="bg-bg-tertiary p-2 rounded shadow">
<MenuItem className='text-center mb-2'>
<strong>{getTag(user)}</strong>
</MenuItem>
<MenuItem
onClick={() => dispatch(openUserProfile(user))}
className="flex items-center justify-between cursor-pointer">
@ -61,16 +64,17 @@ const GuildMemberMenu: React.FunctionComponent<GuildMemberMenuProps> = ({ user }
{(canKick || canManage) && (
<div>
<hr className="my-2 border-bg-primary" />
<Category title="Manage Roles" className="pb-2" />
<Category title="Roles" className="pb-2" />
{perms.can('MANAGE_ROLES', guild.id) && <RoleManager member={member} />}
{(!isSelf && perms.can('KICK_MEMBERS', guild.id)) && (
<MenuItem
className="danger cursor-pointer mb-2"
className="danger cursor-pointer mt-2 pt-1"
onClick={onKickMember}>
<span>Kick {user.username}</span>
<span>Kick {getTag(user)}</span>
</MenuItem>
)}
{perms.can('MANAGE_ROLES', guild.id) && <RoleManager member={member} />}
</div>
)}

View File

@ -18,7 +18,7 @@ const GuildMenu: React.FunctionComponent<GuildMenuProps> = ({ guild }) => {
<ContextMenu
key={guild.id}
id={guild.id}
className="bg-bg-tertiary rounded shadow w-48 p-2">
className="bg-bg-tertiary rounded shadow w-56 p-2">
<MenuItem
className="flex items-center justify-between danger cursor-pointer"
onClick={() => dispatch(leaveGuild(guild.id))}>

View File

@ -16,7 +16,7 @@ const MessageMenu: React.FunctionComponent<MessageMenuProps> = ({ message }) =>
<ContextMenu
key={message.id}
id={message.id}
className="bg-bg-tertiary rounded shadow w-48 p-2">
className="bg-bg-tertiary rounded shadow w-56 p-2">
<div className="overflow-hidden">
<span className="bg-bg-primary p-1 rounded max-w-full">{message.content}</span>
</div>

View File

@ -14,7 +14,7 @@ const RoleMenu: React.FunctionComponent<RoleMenuProps> = ({ role }) => {
<ContextMenu
key={role.id}
id={role.id}
className="bg-bg-tertiary rounded shadow w-48 p-2">
className="bg-bg-tertiary rounded shadow w-56 p-2">
<div style={{ color: role.color }}>{role.name}</div>
{devMode && <DevModeMenuSection ids={[
{ title: 'Role ID', id: role.id },

View File

@ -22,15 +22,15 @@ const ChannelSettings: React.FunctionComponent = () => {
<Modal typeName={'ChannelSettings'} size="full">
<div className="grid grid-cols-12 h-full">
<div className="col-span-4 bg-bg-secondary">
<nav className="float-right flex-grow py-14 w-48 my-1 mr-4">
<nav className="float-right flex-grow py-14 w-1/2 my-1 mr-4">
<Category
className="muted px-2.5 pb-1.5"
title={`#${channel.name}`} />
<NavTabs
tabs={tabs}
tab={tab}
setTab={setTab}
predicate={t => perms.can(t.perm as any, guild.id)} />
<NavTabs
tabs={tabs}
tab={tab}
setTab={setTab}
predicate={t => perms.can(t.perm as any, guild.id)} />
</nav>
</div>
@ -46,5 +46,5 @@ const ChannelSettings: React.FunctionComponent = () => {
</Modal>
) : null;
};
export default ChannelSettings;

View File

@ -18,19 +18,19 @@ const GuildSettings: React.FunctionComponent = () => {
<Modal typeName={'GuildSettings'} size="full">
<div className="grid grid-cols-12 h-full">
<div className="col-span-4 bg-bg-secondary">
<nav className="float-right flex-grow py-14 w-48 my-1 mr-4">
<nav className="float-right flex-grow py-14 w-1/2 my-1 mr-4">
<Category
className="muted px-2.5 pb-1.5"
title={guild.name} />
<NavTabs
tabs={[
{ perm: 'MANAGE_GUILD', name: 'Overview', id: 'overview' },
{ perm: 'MANAGE_ROLES', name: 'Roles', id: 'roles' },
{ perm: 'MANAGE_INVITES', name: 'Invites', id: 'invites' },
]}
tab={tab}
setTab={setTab}
predicate={t => perms.can(t.perm as any, guild.id)} />
<NavTabs
tabs={[
{ perm: 'MANAGE_GUILD', name: 'Overview', id: 'overview' },
{ perm: 'MANAGE_ROLES', name: 'Roles', id: 'roles' },
{ perm: 'MANAGE_INVITES', name: 'Invites', id: 'invites' },
]}
tab={tab}
setTab={setTab}
predicate={t => perms.can(t.perm as any, guild.id)} />
</nav>
</div>
@ -47,5 +47,5 @@ const GuildSettings: React.FunctionComponent = () => {
</Modal>
) : null;
};
export default GuildSettings;

View File

@ -22,7 +22,7 @@ const UserSettings: React.FunctionComponent = () => {
size="full">
<div className="grid grid-cols-12 h-full">
<div className="col-span-4 bg-bg-secondary">
<nav className="float-right flex-grow py-14 w-48 my-1 mr-4">
<nav className="float-right flex-grow py-14 w-1/2 my-1 mr-4">
<Category
className="normal px-2.5 pb-1.5"
title="User Settings" />

View File

@ -9,5 +9,5 @@ const useFormat = () => {
return format.toHTML(mentions.toHTML(content));
};
}
export default useFormat;

View File

@ -1,7 +1,7 @@
import { MentionService } from './mention-service';
import { test, given } from '@acrd/ion';
describe.skip('mention-service', () => {
describe('mention-service', () => {
let service: MentionService;
let state: Store.AppState;

View File

@ -1,9 +1,7 @@
import { getChannel, getChannelByName } from '../store/channels';
import { getTag, getUser, getUserByTag } from '../store/users';
import { getUser, getUserByTag, getUserByUsername } from '../store/users';
export class MentionService {
public readonly tags = ['@245538070684827648', '#\d{18}'];
private readonly patterns = {
formatted: {
channel: /<#(\d{18})>/gm,
@ -12,22 +10,26 @@ export class MentionService {
original: {
channel: /#([A-Za-z\-\d]{2,32})/gm,
user: /@([A-Za-z\d\-\_ ]{2,32}#\d{4})/gm,
userShorthand: /@([A-Za-z\d\-\_ ]{2,32})/gm,
},
};
constructor(private state: Store.AppState) { }
// messageBox.onInput -> formatted mentions appear fancy in message box
public formatOriginal(content: string) {
const guildId = this.state.ui.activeGuild!.id;
return content
.replace(this.patterns.original.channel, (og, name) => {
.replaceAll(this.patterns.original.channel, (og, name) => {
const channel = getChannelByName(guildId, name)(this.state);
return (channel) ? `<#${channel?.id}>` : og;
return (channel) ? `<#${channel.id}>` : og;
})
.replace(this.patterns.original.user, (og, tag) => {
.replaceAll(this.patterns.original.user, (og, tag) => {
const user = getUserByTag(tag)(this.state);
return (user) ? `<@${user.id}>` : og;
})
.replaceAll(this.patterns.original.userShorthand, (og, tag) => {
const user = getUserByUsername(tag)(this.state);
return (user) ? `<@${user.id}>` : og;
});
}
@ -39,22 +41,31 @@ export class MentionService {
private mentionAnchorTag(type: 'channel' | 'user', id: string) {
const selfUserId = this.state.auth.user!.id;
const guildId = this.state.ui.activeGuild!.id;
const channelId = this.state.ui.activeChannel?.id;
const guildId = this.state.ui.activeGuild?.id;
var channel = getChannel(id)(this.state);
var user = getUser(id)(this.state);
const tag = {
channel: {
onClick: `window.location.href = '/channels/${guildId}/${id}'`,
text: `#${getChannel(id)(this.state)?.name}`,
onClick: `window.location.href = '/channels/${guildId}/${id}';`,
text: `#${channel?.name}`,
},
user: {
onClick: `events.emit('openUserProfile', '${id}')`,
text: `@${getTag(getUser(id)(this.state))}`,
text: `@${user?.username}`,
},
};
const mentioned = (id === selfUserId) ? 'bg-tertiary rounded px-1' : '';
return `<a
data-id="${id}"
class="font-extrabold cursor-pointer hover:underline ${mentioned}"
onclick="${tag[type].onClick}">${tag[type].text}</a>`;
const mentioned = (id === selfUserId || id === channelId)
? 'bg-tertiary rounded px-1'
: 'bg-bg-tertiary rounded px-1';
return (user || channel)
? `<a data-id="${id}"
class="font-extrabold cursor-pointer hover:underline ${mentioned}"
onclick="${tag[type].onClick}">${tag[type].text}</a>`
: `<a>Not Found</a>`;
}
}

View File

@ -88,6 +88,10 @@ export const getUserByTag = (tag: string) => createSelector(
return users.find(u => u.username === username && u.discriminator === +discrim);
}
);
export const getUserByUsername = (username: string) => createSelector(
state => state.entities.users,
users => users.find(u => u.username === username)
);
export const getTag = ({ discriminator, username }: Entity.User) => {
const tag = (discriminator || 0)