mirror of https://github.com/vector-im/riot-web
Merge branch 'develop' into gsouquet/fix-thread-root-hidden
commit
904147b194
|
@ -676,10 +676,57 @@ $hover-select-border: 4px;
|
|||
}
|
||||
}
|
||||
|
||||
.mx_ThreadInfo:hover {
|
||||
cursor: pointer;
|
||||
.mx_ThreadInfo {
|
||||
height: 35px;
|
||||
position: relative;
|
||||
background-color: $system;
|
||||
padding-left: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-radius: 8px;
|
||||
padding-right: 16px;
|
||||
padding-top: 8px;
|
||||
padding-bottom: 8px;
|
||||
font-size: 12px;
|
||||
color: $secondary-content;
|
||||
box-sizing: border-box;
|
||||
justify-content: flex-start;
|
||||
|
||||
&:hover, &-active {
|
||||
cursor: pointer;
|
||||
border: 1px solid $quinary-content;
|
||||
padding-top: 7px;
|
||||
padding-bottom: 7px;
|
||||
padding-left: 11px;
|
||||
padding-right: 15px;
|
||||
}
|
||||
|
||||
.mx_ThreadInfo_content {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.mx_ThreadInfo_thread-icon {
|
||||
mask-image: url('$(res)/img/element-icons/thread-summary.svg');
|
||||
mask-position: center;
|
||||
height: 16px;
|
||||
min-width: 16px;
|
||||
background-color: $secondary-content;
|
||||
mask-repeat: no-repeat;
|
||||
mask-size: contain;
|
||||
}
|
||||
.mx_ThreadInfo_threads-amount {
|
||||
font-weight: 600;
|
||||
position: relative;
|
||||
padding: 0 8px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
.mx_ThreadView {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
|
@ -15,7 +15,6 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
.mx_DevicesPanel {
|
||||
display: table;
|
||||
table-layout: fixed;
|
||||
// Normally the panel is 880px, however this can easily overflow the container.
|
||||
// TODO: Fix the table to not be squishy
|
||||
|
@ -25,16 +24,17 @@ limitations under the License.
|
|||
}
|
||||
|
||||
.mx_DevicesPanel_header {
|
||||
display: table-header-group;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.mx_DevicesPanel_header > .mx_DevicesPanel_deviceButtons {
|
||||
.mx_DevicesPanel_header .mx_DevicesPanel_deviceButtons {
|
||||
height: 48px; // make this tall so the table doesn't move down when the delete button appears
|
||||
width: 20%;
|
||||
}
|
||||
|
||||
.mx_DevicesPanel_header > div {
|
||||
display: table-cell;
|
||||
.mx_DevicesPanel_header th {
|
||||
padding: 0px;
|
||||
text-align: left;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
|
@ -46,16 +46,9 @@ limitations under the License.
|
|||
width: 30%;
|
||||
}
|
||||
|
||||
.mx_DevicesPanel_header .mx_DevicesPanel_deviceButtons {
|
||||
width: 20%;
|
||||
}
|
||||
|
||||
.mx_DevicesPanel_device {
|
||||
display: table-row;
|
||||
}
|
||||
|
||||
.mx_DevicesPanel_device > div {
|
||||
display: table-cell;
|
||||
.mx_DevicesPanel_device td {
|
||||
vertical-align: baseline;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.mx_DevicesPanel_myDevice {
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="none" viewBox="0 0 18 18"><path fill="#17191C" fill-rule="evenodd" d="M3 .25A2.75 2.75 0 0 0 .25 3v14a.75.75 0 0 0 1.2.6L4.916 15c.217-.162.48-.25.75-.25H15A2.75 2.75 0 0 0 17.75 12V3A2.75 2.75 0 0 0 15 .25H3ZM4.25 6A.75.75 0 0 1 5 5.25h8a.75.75 0 0 1 0 1.5H5A.75.75 0 0 1 4.25 6ZM5 8.25a.75.75 0 0 0 0 1.5h4a.75.75 0 1 0 0-1.5H5Z" clip-rule="evenodd"/></svg>
|
After Width: | Height: | Size: 428 B |
|
@ -35,6 +35,7 @@ import Spinner from './Spinner';
|
|||
import ReplyTile from "../rooms/ReplyTile";
|
||||
import Pill from './Pill';
|
||||
import { Room } from 'matrix-js-sdk/src/models/room';
|
||||
import { RelationType } from 'matrix-js-sdk/src/@types/event';
|
||||
|
||||
/**
|
||||
* This number is based on the previous behavior - if we have message of height
|
||||
|
@ -226,13 +227,30 @@ export default class ReplyThread extends React.Component<IProps, IState> {
|
|||
|
||||
public static makeReplyMixIn(ev: MatrixEvent) {
|
||||
if (!ev) return {};
|
||||
return {
|
||||
|
||||
const mixin: any = {
|
||||
'm.relates_to': {
|
||||
'm.in_reply_to': {
|
||||
'event_id': ev.getId(),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* If the event replied is part of a thread
|
||||
* Add the `m.thread` relation so that clients
|
||||
* that know how to handle that relation will
|
||||
* be able to render them more accurately
|
||||
*/
|
||||
if (ev.isThreadRelation) {
|
||||
mixin['m.relates_to'] = {
|
||||
...mixin['m.relates_to'],
|
||||
rel_type: RelationType.Thread,
|
||||
event_id: ev.threadRootId,
|
||||
};
|
||||
}
|
||||
|
||||
return mixin;
|
||||
}
|
||||
|
||||
public static hasThreadReply(event: MatrixEvent) {
|
||||
|
|
|
@ -20,7 +20,7 @@ import React from "react";
|
|||
import { RovingAccessibleTooltipButton } from "../../../accessibility/RovingTabIndex";
|
||||
import Spinner from "../elements/Spinner";
|
||||
import classNames from "classnames";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { _t, _td } from "../../../languageHandler";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { FileDownloader } from "../../../utils/FileDownloader";
|
||||
|
||||
|
@ -36,6 +36,7 @@ interface IProps {
|
|||
interface IState {
|
||||
loading: boolean;
|
||||
blob?: Blob;
|
||||
tooltip: string;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.messages.DownloadActionButton")
|
||||
|
@ -47,12 +48,17 @@ export default class DownloadActionButton extends React.PureComponent<IProps, IS
|
|||
|
||||
this.state = {
|
||||
loading: false,
|
||||
tooltip: _td("Downloading"),
|
||||
};
|
||||
}
|
||||
|
||||
private onDownloadClick = async () => {
|
||||
if (this.state.loading) return;
|
||||
|
||||
if (this.props.mediaEventHelperGet().media.isEncrypted) {
|
||||
this.setState({ tooltip: _td("Decrypting") });
|
||||
}
|
||||
|
||||
this.setState({ loading: true });
|
||||
|
||||
if (this.state.blob) {
|
||||
|
@ -87,7 +93,7 @@ export default class DownloadActionButton extends React.PureComponent<IProps, IS
|
|||
|
||||
return <RovingAccessibleTooltipButton
|
||||
className={classes}
|
||||
title={spinner ? _t("Decrypting") : _t("Download")}
|
||||
title={spinner ? _t(this.state.tooltip) : _t("Download")}
|
||||
onClick={this.onDownloadClick}
|
||||
disabled={!!spinner}
|
||||
>
|
||||
|
|
|
@ -59,6 +59,7 @@ import { getEventDisplayInfo } from '../../../utils/EventUtils';
|
|||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import MKeyVerificationConclusion from "../messages/MKeyVerificationConclusion";
|
||||
import { dispatchShowThreadEvent } from '../../../dispatcher/dispatch-actions/threads';
|
||||
import { MessagePreviewStore } from '../../../stores/room-list/MessagePreviewStore';
|
||||
|
||||
const eventTileTypes = {
|
||||
[EventType.RoomMessage]: 'messages.MessageEvent',
|
||||
|
@ -556,10 +557,9 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
return null;
|
||||
}
|
||||
|
||||
const avatars = Array.from(thread.participants).map((mxId: string) => {
|
||||
const member = room.getMember(mxId);
|
||||
return <MemberAvatar key={member.userId} member={member} width={14} height={14} />;
|
||||
});
|
||||
const threadMessagePreview = MessagePreviewStore.instance.generateThreadPreview(this.state.thread);
|
||||
|
||||
if (!threadMessagePreview) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
|
@ -568,10 +568,18 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
dispatchShowThreadEvent(this.props.mxEvent);
|
||||
}}
|
||||
>
|
||||
<span className="mx_EventListSummary_avatars">
|
||||
{ avatars }
|
||||
<span className="mx_ThreadInfo_thread-icon" />
|
||||
<span className="mx_ThreadInfo_threads-amount">
|
||||
{ _t("%(count)s reply", {
|
||||
count: thread.length - 1,
|
||||
}) }
|
||||
</span>
|
||||
{ thread.length - 1 } { thread.length === 2 ? 'reply' : 'replies' }
|
||||
<MemberAvatar member={thread.replyToEvent.sender} width={24} height={24} />
|
||||
<div className="mx_ThreadInfo_content">
|
||||
<span className="mx_ThreadInfo_message-preview">
|
||||
{ threadMessagePreview }
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -218,17 +218,21 @@ export default class DevicesPanel extends React.Component<IProps, IState> {
|
|||
|
||||
const classes = classNames(this.props.className, "mx_DevicesPanel");
|
||||
return (
|
||||
<div className={classes}>
|
||||
<div className="mx_DevicesPanel_header">
|
||||
<div className="mx_DevicesPanel_deviceId">{ _t("ID") }</div>
|
||||
<div className="mx_DevicesPanel_deviceName">{ _t("Public Name") }</div>
|
||||
<div className="mx_DevicesPanel_deviceLastSeen">{ _t("Last seen") }</div>
|
||||
<div className="mx_DevicesPanel_deviceButtons">
|
||||
{ this.state.selectedDevices.length > 0 ? deleteButton : null }
|
||||
</div>
|
||||
</div>
|
||||
{ devices.map(this.renderDevice) }
|
||||
</div>
|
||||
<table className={classes}>
|
||||
<thead className="mx_DevicesPanel_header">
|
||||
<tr>
|
||||
<th className="mx_DevicesPanel_deviceId">{ _t("ID") }</th>
|
||||
<th className="mx_DevicesPanel_deviceName">{ _t("Public Name") }</th>
|
||||
<th className="mx_DevicesPanel_deviceLastSeen">{ _t("Last seen") }</th>
|
||||
<th className="mx_DevicesPanel_deviceButtons">
|
||||
{ this.state.selectedDevices.length > 0 ? deleteButton : null }
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{ devices.map(this.renderDevice) }
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -66,23 +66,23 @@ export default class DevicesPanelEntry extends React.Component<IProps> {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className={"mx_DevicesPanel_device" + myDeviceClass}>
|
||||
<div className="mx_DevicesPanel_deviceId">
|
||||
<tr className={"mx_DevicesPanel_device" + myDeviceClass}>
|
||||
<td className="mx_DevicesPanel_deviceId">
|
||||
{ device.device_id }
|
||||
</div>
|
||||
<div className="mx_DevicesPanel_deviceName">
|
||||
</td>
|
||||
<td className="mx_DevicesPanel_deviceName">
|
||||
<EditableTextContainer initialValue={device.display_name}
|
||||
onSubmit={this.onDisplayNameChanged}
|
||||
placeholder={device.device_id}
|
||||
/>
|
||||
</div>
|
||||
<div className="mx_DevicesPanel_lastSeen">
|
||||
</td>
|
||||
<td className="mx_DevicesPanel_lastSeen">
|
||||
{ lastSeen }
|
||||
</div>
|
||||
<div className="mx_DevicesPanel_deviceButtons">
|
||||
</td>
|
||||
<td className="mx_DevicesPanel_deviceButtons">
|
||||
<StyledCheckbox onChange={this.onDeviceToggled} checked={this.props.selected} />
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1551,6 +1551,8 @@
|
|||
"Send as message": "Send as message",
|
||||
"Edit message": "Edit message",
|
||||
"Mod": "Mod",
|
||||
"%(count)s reply|other": "%(count)s replies",
|
||||
"%(count)s reply|one": "%(count)s reply",
|
||||
"This event could not be displayed": "This event could not be displayed",
|
||||
"Your key share request has been sent - please check your other sessions for key share requests.": "Your key share request has been sent - please check your other sessions for key share requests.",
|
||||
"Key share requests are sent to your other sessions automatically. If you rejected or dismissed the key share request on your other sessions, click here to request the keys for this session again.": "Key share requests are sent to your other sessions automatically. If you rejected or dismissed the key share request on your other sessions, click here to request the keys for this session again.",
|
||||
|
@ -1959,6 +1961,7 @@
|
|||
"Saturday": "Saturday",
|
||||
"Today": "Today",
|
||||
"Yesterday": "Yesterday",
|
||||
"Downloading": "Downloading",
|
||||
"Decrypting": "Decrypting",
|
||||
"Download": "Download",
|
||||
"View Source": "View Source",
|
||||
|
|
|
@ -27,6 +27,7 @@ import { CallHangupEvent } from "./previews/CallHangupEvent";
|
|||
import { StickerEventPreview } from "./previews/StickerEventPreview";
|
||||
import { ReactionEventPreview } from "./previews/ReactionEventPreview";
|
||||
import { UPDATE_EVENT } from "../AsyncStore";
|
||||
import { Thread } from "matrix-js-sdk/src/models/thread";
|
||||
|
||||
// Emitted event for when a room's preview has changed. First argument will the room for which
|
||||
// the change happened.
|
||||
|
@ -108,6 +109,15 @@ export class MessagePreviewStore extends AsyncStoreWithClient<IState> {
|
|||
return previews.get(inTagId);
|
||||
}
|
||||
|
||||
public generateThreadPreview(thread: Thread): string {
|
||||
const lastEvent = thread.replyToEvent;
|
||||
const previewDef = PREVIEWS[lastEvent.getType()];
|
||||
// TODO: Handle case where we don't have
|
||||
if (!previewDef) return '';
|
||||
const previewText = previewDef.previewer.getTextFor(lastEvent, null, true);
|
||||
return previewText ?? '';
|
||||
}
|
||||
|
||||
private async generatePreview(room: Room, tagId?: TagID) {
|
||||
const events = room.timeline;
|
||||
if (!events) return; // should only happen in tests
|
||||
|
|
|
@ -23,7 +23,7 @@ import ReplyThread from "../../../components/views/elements/ReplyThread";
|
|||
import { getHtmlText } from "../../../HtmlUtils";
|
||||
|
||||
export class MessageEventPreview implements IPreview {
|
||||
public getTextFor(event: MatrixEvent, tagId?: TagID): string {
|
||||
public getTextFor(event: MatrixEvent, tagId?: TagID, isThread?: boolean): string {
|
||||
let eventContent = event.getContent();
|
||||
|
||||
if (event.isRelation("m.replace")) {
|
||||
|
@ -64,7 +64,7 @@ export class MessageEventPreview implements IPreview {
|
|||
return _t("* %(senderName)s %(emote)s", { senderName: getSenderName(event), emote: body });
|
||||
}
|
||||
|
||||
if (isSelf(event) || !shouldPrefixMessagesIn(event.getRoomId(), tagId)) {
|
||||
if (isThread || isSelf(event) || !shouldPrefixMessagesIn(event.getRoomId(), tagId)) {
|
||||
return body;
|
||||
} else {
|
||||
return _t("%(senderName)s: %(message)s", { senderName: getSenderName(event), message: body });
|
||||
|
|
|
@ -23,7 +23,7 @@ import SettingsStore from "../../../settings/SettingsStore";
|
|||
import DMRoomMap from "../../../utils/DMRoomMap";
|
||||
|
||||
export class ReactionEventPreview implements IPreview {
|
||||
public getTextFor(event: MatrixEvent, tagId?: TagID): string {
|
||||
public getTextFor(event: MatrixEvent, tagId?: TagID, isThread?: boolean): string {
|
||||
const showDms = SettingsStore.getValue("feature_roomlist_preview_reactions_dms");
|
||||
const showAll = SettingsStore.getValue("feature_roomlist_preview_reactions_all");
|
||||
|
||||
|
@ -41,7 +41,7 @@ export class ReactionEventPreview implements IPreview {
|
|||
const reaction = relation.key;
|
||||
if (!reaction) return null; // invalid reaction (unknown format)
|
||||
|
||||
if (isSelf(event) || !shouldPrefixMessagesIn(event.getRoomId(), tagId)) {
|
||||
if (isThread || isSelf(event) || !shouldPrefixMessagesIn(event.getRoomId(), tagId)) {
|
||||
return reaction;
|
||||
} else {
|
||||
return _t("%(senderName)s: %(reaction)s", { senderName: getSenderName(event), reaction });
|
||||
|
|
|
@ -21,11 +21,11 @@ import { getSenderName, isSelf, shouldPrefixMessagesIn } from "./utils";
|
|||
import { _t } from "../../../languageHandler";
|
||||
|
||||
export class StickerEventPreview implements IPreview {
|
||||
public getTextFor(event: MatrixEvent, tagId?: TagID): string {
|
||||
public getTextFor(event: MatrixEvent, tagId?: TagID, isThread?: boolean): string {
|
||||
const stickerName = event.getContent()['body'];
|
||||
if (!stickerName) return null;
|
||||
|
||||
if (isSelf(event) || !shouldPrefixMessagesIn(event.getRoomId(), tagId)) {
|
||||
if (isThread || isSelf(event) || !shouldPrefixMessagesIn(event.getRoomId(), tagId)) {
|
||||
return stickerName;
|
||||
} else {
|
||||
return _t("%(senderName)s: %(stickerName)s", { senderName: getSenderName(event), stickerName });
|
||||
|
|
Loading…
Reference in New Issue