0.5.0-pre-release
This commit is contained in:
parent
d47476d5b9
commit
5ae54305f4
@ -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.
|
||||
|
||||

|
||||
|
||||
### 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
|
||||
|
@ -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 '';
|
||||
}
|
||||
}
|
2
frontend/env/.env.dev
vendored
2
frontend/env/.env.dev
vendored
@ -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"
|
2
frontend/env/.env.prod
vendored
2
frontend/env/.env.prod
vendored
@ -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"
|
@ -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.`;
|
||||
|
@ -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> •
|
||||
|
@ -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 = () => (
|
||||
<>
|
||||
|
@ -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>;
|
||||
|
@ -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>
|
||||
)}
|
||||
|
||||
|
@ -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))}>
|
||||
|
@ -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>
|
||||
|
@ -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 },
|
||||
|
@ -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;
|
@ -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;
|
@ -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" />
|
||||
|
@ -9,5 +9,5 @@ const useFormat = () => {
|
||||
return format.toHTML(mentions.toHTML(content));
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export default useFormat;
|
@ -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;
|
||||
|
||||
|
@ -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>`;
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user