mirror of
https://github.com/element-hq/element-web.git
synced 2024-12-18 19:51:47 +03:00
Fix outstanding UX issues with replies/mentions/keyword notifs
This commit is contained in:
parent
d4cf3881bc
commit
3541391747
10 changed files with 121 additions and 12 deletions
|
@ -26,7 +26,8 @@ Please see LICENSE files in the repository root for full details.
|
|||
}
|
||||
|
||||
&.mx_UserPill_me,
|
||||
&.mx_AtRoomPill {
|
||||
&.mx_AtRoomPill,
|
||||
&.mx_KeywordPill {
|
||||
background-color: var(--cpd-color-bg-critical-primary) !important; /* To override .markdown-body */
|
||||
}
|
||||
|
||||
|
@ -45,7 +46,8 @@ Please see LICENSE files in the repository root for full details.
|
|||
}
|
||||
|
||||
/* We don't want to indicate clickability */
|
||||
&.mx_AtRoomPill:hover {
|
||||
&.mx_AtRoomPill:hover,
|
||||
&.mx_KeywordPill:hover {
|
||||
background-color: var(--cpd-color-bg-critical-primary) !important; /* To override .markdown-body */
|
||||
cursor: unset;
|
||||
}
|
||||
|
|
|
@ -135,12 +135,6 @@ $left-gutter: 64px;
|
|||
}
|
||||
}
|
||||
|
||||
&.mx_EventTile_highlight,
|
||||
&.mx_EventTile_highlight .markdown-body,
|
||||
&.mx_EventTile_highlight .mx_EventTile_edited {
|
||||
color: $alert;
|
||||
}
|
||||
|
||||
&.mx_EventTile_bubbleContainer {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 100px;
|
||||
|
@ -584,6 +578,14 @@ $left-gutter: 64px;
|
|||
padding-inline-start: calc(var(--EventTile_group_line-spacing-inline-start) + 20px);
|
||||
}
|
||||
}
|
||||
|
||||
&.mx_EventTile_highlight,
|
||||
&.mx_EventTile_highlight .markdown-body {
|
||||
> .mx_EventTile_line {
|
||||
box-shadow: inset var(--EventTile-box-shadow-offset-x) 0 0 var(--EventTile-box-shadow-spread-radius)
|
||||
$event-highlight-edge-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&[data-layout="bubble"] {
|
||||
|
|
|
@ -83,6 +83,7 @@ $spacePanel-bg-color: rgba(38, 39, 43, 0.82);
|
|||
$panel-gradient: rgba(34, 38, 46, 0), rgba(34, 38, 46, 1);
|
||||
$h3-color: $primary-content;
|
||||
$event-highlight-bg-color: #25271f;
|
||||
$event-highlight-edge-color: #f0b132;
|
||||
$header-panel-text-primary-color: $text-secondary-color;
|
||||
/* ******************** */
|
||||
|
||||
|
|
|
@ -164,6 +164,7 @@ $widget-menu-bar-bg-color: $header-panel-bg-color;
|
|||
$widget-body-bg-color: #1a1d23;
|
||||
|
||||
$event-highlight-bg-color: #25271f;
|
||||
$event-highlight-edge-color: #f0b132;
|
||||
|
||||
/* event timestamp */
|
||||
$event-timestamp-color: $text-secondary-color;
|
||||
|
|
|
@ -235,6 +235,7 @@ $widget-body-bg-color: #fff;
|
|||
$yellow-background: #fff8e3;
|
||||
|
||||
$event-highlight-bg-color: $yellow-background;
|
||||
$event-highlight-edge-color: var(--cpd-color-yellow-500);
|
||||
|
||||
/* event timestamp */
|
||||
$event-timestamp-color: #acacac;
|
||||
|
|
|
@ -100,6 +100,7 @@ $button-danger-disabled-bg-color: var(--warning-color-50pct); /* still needs alp
|
|||
/* --timeline-highlights-color */
|
||||
$event-selected-color: var(--timeline-highlights-color);
|
||||
$event-highlight-bg-color: var(--timeline-highlights-color);
|
||||
$event-highlight-edge-color: var(--timeline-highlights-color);
|
||||
|
||||
/* redirect some variables away from their hardcoded values in the light theme */
|
||||
$settings-grey-fg-color: $primary-content;
|
||||
|
|
|
@ -112,6 +112,7 @@ $spacePanel-bg-color: rgba(232, 232, 232, 0.77);
|
|||
$panel-gradient: rgba(242, 245, 248, 0), rgba(242, 245, 248, 1);
|
||||
$h3-color: #3d3b39;
|
||||
$event-highlight-bg-color: $yellow-background;
|
||||
$event-highlight-edge-color: var(--cpd-color-yellow-500);
|
||||
$header-panel-text-primary-color: #91a1c0;
|
||||
/* ******************** */
|
||||
|
||||
|
|
|
@ -25,6 +25,7 @@ export enum PillType {
|
|||
AtRoomMention = "TYPE_AT_ROOM_MENTION", // '@room' mention
|
||||
EventInSameRoom = "TYPE_EVENT_IN_SAME_ROOM",
|
||||
EventInOtherRoom = "TYPE_EVENT_IN_OTHER_ROOM",
|
||||
Keyword = "TYPE_KEYWORD", // Used to highlight keywords that triggered a notification rule
|
||||
}
|
||||
|
||||
export const pillRoomNotifPos = (text: string | null): number => {
|
||||
|
@ -76,14 +77,32 @@ export interface PillProps {
|
|||
room?: Room;
|
||||
// Whether to include an avatar in the pill
|
||||
shouldShowPillAvatar?: boolean;
|
||||
// Explicitly-provided text to display in the pill
|
||||
text?: string;
|
||||
}
|
||||
|
||||
export const Pill: React.FC<PillProps> = ({ type: propType, url, inMessage, room, shouldShowPillAvatar = true }) => {
|
||||
const { event, member, onClick, resourceId, targetRoom, text, type } = usePermalink({
|
||||
export const Pill: React.FC<PillProps> = ({
|
||||
type: propType,
|
||||
url,
|
||||
inMessage,
|
||||
room,
|
||||
shouldShowPillAvatar = true,
|
||||
text: customPillText,
|
||||
}) => {
|
||||
const {
|
||||
event,
|
||||
member,
|
||||
onClick,
|
||||
resourceId,
|
||||
targetRoom,
|
||||
text: linkText,
|
||||
type,
|
||||
} = usePermalink({
|
||||
room,
|
||||
type: propType,
|
||||
url,
|
||||
});
|
||||
const text = customPillText ?? linkText;
|
||||
|
||||
if (!type || !text) {
|
||||
return null;
|
||||
|
@ -96,6 +115,7 @@ export const Pill: React.FC<PillProps> = ({ type: propType, url, inMessage, room
|
|||
mx_UserPill: type === PillType.UserMention,
|
||||
mx_UserPill_me: resourceId === MatrixClientPeg.safeGet().getUserId(),
|
||||
mx_EventPill: type === PillType.EventInOtherRoom || type === PillType.EventInSameRoom,
|
||||
mx_KeywordPill: type === PillType.Keyword,
|
||||
});
|
||||
|
||||
let avatar: ReactElement | null = null;
|
||||
|
@ -131,6 +151,8 @@ export const Pill: React.FC<PillProps> = ({ type: propType, url, inMessage, room
|
|||
case PillType.UserMention:
|
||||
avatar = <PillMemberAvatar shouldShowPillAvatar={shouldShowPillAvatar} member={member} />;
|
||||
break;
|
||||
case PillType.Keyword:
|
||||
break;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -8,8 +8,9 @@ Please see LICENSE files in the repository root for full details.
|
|||
|
||||
import React, { createRef, SyntheticEvent, MouseEvent } from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import { MsgType } from "matrix-js-sdk/src/matrix";
|
||||
import { MsgType, PushRuleKind } from "matrix-js-sdk/src/matrix";
|
||||
import { TooltipProvider } from "@vector-im/compound-web";
|
||||
import { globToRegexp } from "matrix-js-sdk/src/utils";
|
||||
|
||||
import * as HtmlUtils from "../../../HtmlUtils";
|
||||
import { formatDate } from "../../../DateUtils";
|
||||
|
@ -36,6 +37,7 @@ import { EditWysiwygComposer } from "../rooms/wysiwyg_composer";
|
|||
import { IEventTileOps } from "../rooms/EventTile";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import CodeBlock from "./CodeBlock";
|
||||
import { Pill, PillType } from "../elements/Pill";
|
||||
|
||||
interface IState {
|
||||
// the URLs (if any) to be previewed with a LinkPreviewWidget inside this TextualBody.
|
||||
|
@ -102,6 +104,16 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Highlight notification keywords using pills
|
||||
const pushDetails = this.props.mxEvent.getPushDetails();
|
||||
if (
|
||||
pushDetails.rule?.enabled &&
|
||||
pushDetails.rule.kind === PushRuleKind.ContentSpecific &&
|
||||
pushDetails.rule.pattern
|
||||
) {
|
||||
this.pillifyNotificationKeywords([content], this.regExpForKeywordPattern(pushDetails.rule.pattern));
|
||||
}
|
||||
}
|
||||
|
||||
private addCodeElement(pre: HTMLPreElement): void {
|
||||
|
@ -211,6 +223,55 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks the text that activated a push-notification keyword pattern.
|
||||
*/
|
||||
private pillifyNotificationKeywords(nodes: ArrayLike<Element>, exp: RegExp): void {
|
||||
let node: Node | null = nodes[0];
|
||||
while (node) {
|
||||
if (node.nodeType === Node.TEXT_NODE) {
|
||||
const text = node.nodeValue;
|
||||
if (!text) {
|
||||
node = node.nextSibling;
|
||||
continue;
|
||||
}
|
||||
const match = text.match(exp);
|
||||
if (!match || match.length < 3) {
|
||||
node = node.nextSibling;
|
||||
continue;
|
||||
}
|
||||
const keywordText = match[2];
|
||||
const idx = match.index! + match[1].length;
|
||||
const before = text.substring(0, idx);
|
||||
const after = text.substring(idx + keywordText.length);
|
||||
|
||||
const container = document.createElement("span");
|
||||
const newContent = (
|
||||
<>
|
||||
{before}
|
||||
<TooltipProvider>
|
||||
<Pill text={keywordText} type={PillType.Keyword} />
|
||||
</TooltipProvider>
|
||||
{after}
|
||||
</>
|
||||
);
|
||||
ReactDOM.render(newContent, container);
|
||||
|
||||
node.parentNode?.replaceChild(container, node);
|
||||
} else if (node.childNodes && node.childNodes.length) {
|
||||
this.pillifyNotificationKeywords(node.childNodes as NodeListOf<Element>, exp);
|
||||
}
|
||||
|
||||
node = node.nextSibling;
|
||||
}
|
||||
}
|
||||
|
||||
private regExpForKeywordPattern(pattern: string): RegExp {
|
||||
// Reflects the push notification pattern-matching implementation at
|
||||
// https://github.com/matrix-org/matrix-js-sdk/blob/dbd7d26968b94700827bac525c39afff2c198e61/src/pushprocessor.ts#L570
|
||||
return new RegExp("(^|\\W)(" + globToRegexp(pattern) + ")(\\W|$)", "i");
|
||||
}
|
||||
|
||||
private findLinks(nodes: ArrayLike<Element>): string[] {
|
||||
let links: string[] = [];
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details.
|
|||
*/
|
||||
|
||||
import React from "react";
|
||||
import { MatrixClient, MatrixEvent } from "matrix-js-sdk/src/matrix";
|
||||
import { MatrixClient, MatrixEvent, PushRuleKind } from "matrix-js-sdk/src/matrix";
|
||||
import { mocked, MockedObject } from "jest-mock";
|
||||
import { render, waitFor } from "jest-matrix-react";
|
||||
|
||||
|
@ -228,6 +228,23 @@ describe("<TextualBody />", () => {
|
|||
const content = container.querySelector(".mx_EventTile_body");
|
||||
expect(content.innerHTML.replace(defaultEvent.getId(), "%event_id%")).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should pillify a keyword responsible for triggering a notification", () => {
|
||||
const ev = mkRoomTextMessage("foo bar baz");
|
||||
ev.setPushDetails(undefined, {
|
||||
actions: [],
|
||||
pattern: "bar",
|
||||
rule_id: "bar",
|
||||
default: false,
|
||||
enabled: true,
|
||||
kind: PushRuleKind.ContentSpecific,
|
||||
});
|
||||
const { container } = getComponent({ mxEvent: ev });
|
||||
const content = container.querySelector(".mx_EventTile_body");
|
||||
expect(content.innerHTML).toMatchInlineSnapshot(
|
||||
`"<span>foo <bdi><span tabindex="0"><span class="mx_Pill mx_KeywordPill"><span class="mx_Pill_text">bar</span></span></span></bdi> baz</span>"`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("renders formatted m.text correctly", () => {
|
||||
|
|
Loading…
Reference in a new issue