Iterate with new buttons and resize locking

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
Michael Telatynski 2020-10-15 11:14:48 +01:00
parent d36fafd0c6
commit a6c81a903c
8 changed files with 173 additions and 89 deletions

View file

@ -109,20 +109,55 @@ limitations under the License.
}
.mx_RoomSummaryCard_appsGroup {
.mx_RoomSummaryCard_widgetRow {
margin: 0;
display: flex;
.mx_RoomSummaryCard_Button {
// this button is special so we have to override some of the original styling
// as we will be applying it in its children
padding: 0;
height: auto;
color: $tertiary-fg-color;
.mx_RoomSummaryCard_icon_app {
padding: 8px 48px 8px 12px;
text-overflow: ellipsis;
overflow: hidden;
.mx_BaseAvatar_image {
vertical-align: top;
margin-right: 12px;
}
span {
color: $primary-fg-color;
}
}
.mx_RoomSummaryCard_app_pinToggle,
.mx_RoomSummaryCard_app_options {
position: relative;
height: 16px;
width: 16px;
padding: 8px;
border-radius: 8px;
position: absolute;
top: 0;
height: 100%; // to give bigger interactive zone
width: 24px;
padding: 10px 4px;
box-sizing: border-box;
min-width: 24px; // prevent flexbox crushing
.mx_AccessibleTooltipButton_container {
// TODO
position: absolute;
top: -50px;
}
&:hover {
background-color: rgba(141, 151, 165, 0.1);
&::after {
content: '';
position: absolute;
height: 24px;
width: 24px;
top: 6px; // equal to top-margin of parent
left: 0;
border-radius: 12px;
background-color: rgba(141, 151, 165, 0.1);
}
}
&::before {
@ -138,36 +173,40 @@ limitations under the License.
}
.mx_RoomSummaryCard_app_pinToggle {
right: 24px;
&::before {
mask-image: url('$(res)/img/element-icons/room/pin-upright.svg');
}
&.mx_RoomSummaryCard_app_pinned {
&::before {
background-color: $accent-color;
}
}
}
.mx_RoomSummaryCard_app_options {
right: 48px;
display: none;
&::before {
mask-image: url('$(res)/img/element-icons/room/ellipsis.svg');
}
}
}
.mx_RoomSummaryCard_Button {
padding: 6px 24px 6px 12px;
color: $tertiary-fg-color;
flex: 1;
&.mx_RoomSummaryCard_Button_pinned {
&::after {
opacity: 0.2;
}
span {
color: $primary-fg-color;
.mx_RoomSummaryCard_app_pinToggle::before {
background-color: $accent-color;
}
}
.mx_BaseAvatar_image {
vertical-align: top;
margin-right: 12px;
&:hover {
.mx_RoomSummaryCard_icon_app {
padding-right: 72px;
}
.mx_RoomSummaryCard_app_options {
display: unset;
}
}
&::before {
@ -175,7 +214,8 @@ limitations under the License.
}
&::after {
top: 6px; // re-align based on the height change
top: 8px; // re-align based on the height change
pointer-events: none; // pass through to the real button
}
}
}

View file

@ -276,7 +276,7 @@ export default class RoomView extends React.Component<IProps, IState> {
private checkWidgets = (room) => {
this.setState({
hasPinnedWidgets: WidgetStore.instance.getApps(room, true).length > 0,
hasPinnedWidgets: WidgetStore.instance.getPinnedApps(room.roomId).length > 0,
})
};

View file

@ -26,6 +26,7 @@ interface ITooltipProps extends React.ComponentProps<typeof AccessibleButton> {
tooltip?: React.ReactNode;
tooltipClassName?: string;
forceHide?: boolean;
yOffset?: number;
}
interface IState {
@ -63,12 +64,13 @@ export default class AccessibleTooltipButton extends React.PureComponent<IToolti
render() {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const {title, tooltip, children, tooltipClassName, forceHide, ...props} = this.props;
const {title, tooltip, children, tooltipClassName, forceHide, yOffset, ...props} = this.props;
const tip = this.state.hover ? <Tooltip
className="mx_AccessibleTooltipButton_container"
tooltipClassName={classNames("mx_AccessibleTooltipButton_tooltip", tooltipClassName)}
label={tooltip || title}
yOffset={yOffset}
/> : <div />;
return (
<AccessibleButton

View file

@ -36,6 +36,7 @@ interface IProps {
// the react element to put into the tooltip
label: React.ReactNode;
forceOnRight?: boolean;
yOffset?: number;
}
export default class Tooltip extends React.Component<IProps> {
@ -46,6 +47,7 @@ export default class Tooltip extends React.Component<IProps> {
public static readonly defaultProps = {
visible: true,
yOffset: 0,
};
// Create a wrapper for the tooltip outside the parent and attach it to the body element
@ -82,9 +84,9 @@ export default class Tooltip extends React.Component<IProps> {
offset = Math.floor(parentBox.height - MIN_TOOLTIP_HEIGHT);
}
style.top = (parentBox.top - 2) + window.pageYOffset + offset;
style.top = (parentBox.top - 2 + this.props.yOffset) + window.pageYOffset + offset;
if (!this.props.forceOnRight && parentBox.right > window.innerWidth / 2) {
style.right = window.innerWidth - parentBox.right - window.pageXOffset - 8;
style.right = window.innerWidth - parentBox.right - window.pageXOffset - 16;
} else {
style.left = parentBox.right + window.pageXOffset + 6;
}

View file

@ -43,7 +43,7 @@ import { E2EStatus } from "../../../utils/ShieldUtils";
import RoomContext from "../../../contexts/RoomContext";
import {UIFeature} from "../../../settings/UIFeature";
import {ContextMenuButton} from "../../../accessibility/context_menu/ContextMenuButton";
import {ChevronFace, useContextMenu} from "../../structures/ContextMenu";
import {ChevronFace, ContextMenuTooltipButton, useContextMenu} from "../../structures/ContextMenu";
import WidgetContextMenu from "../context_menus/WidgetContextMenu";
interface IProps {
@ -70,11 +70,11 @@ const Button: React.FC<IButtonProps> = ({ children, className, onClick }) => {
};
export const useWidgets = (room: Room) => {
const [apps, setApps] = useState<IApp[]>(WidgetStore.instance.getApps(room));
const [apps, setApps] = useState<IApp[]>(WidgetStore.instance.getApps(room.roomId));
const updateApps = useCallback(() => {
// Copy the array so that we always trigger a re-render, as some updates mutate the array of apps/settings
setApps([...WidgetStore.instance.getApps(room)]);
setApps([...WidgetStore.instance.getApps(room.roomId)]);
}, [room]);
useEffect(updateApps, [room]);
@ -130,34 +130,39 @@ const AppRow: React.FC<IAppRowProps> = ({ app }) => {
pinTitle = isPinned ? _t("Unpin") : _t("Pin");
}
return <div className="mx_RoomSummaryCard_widgetRow" ref={handle}>
const classes = classNames("mx_BaseCard_Button mx_RoomSummaryCard_Button", {
mx_RoomSummaryCard_Button_pinned: isPinned,
});
return <div className={classes} ref={handle}>
<AccessibleTooltipButton
className="mx_BaseCard_Button mx_RoomSummaryCard_Button mx_RoomSummaryCard_icon_app"
className="mx_RoomSummaryCard_icon_app"
onClick={onOpenWidgetClick}
// only show a tooltip if the widget is pinned
title={isPinned ? _t("Unpin a widget to view it in this panel") : ""}
forceHide={!isPinned}
disabled={isPinned}
yOffset={-48}
>
<WidgetAvatar app={app} />
<span>{name}</span>
{ subtitle }
</AccessibleTooltipButton>
<AccessibleTooltipButton
className={classNames("mx_RoomSummaryCard_app_pinToggle", {
mx_RoomSummaryCard_app_pinned: isPinned,
})}
onClick={togglePin}
title={pinTitle}
disabled={cannotPin}
/>
<ContextMenuButton
<ContextMenuTooltipButton
className="mx_RoomSummaryCard_app_options"
isExpanded={menuDisplayed}
onClick={openMenu}
label={_t("Options")}
title={_t("Options")}
yOffset={-24}
/>
<AccessibleTooltipButton
className="mx_RoomSummaryCard_app_pinToggle"
onClick={togglePin}
title={pinTitle}
disabled={cannotPin}
yOffset={-24}
/>
{ contextMenu }

View file

@ -93,8 +93,9 @@ export default class AppsDrawer extends React.Component {
onResizeStop: () => {
this._resizeContainer.classList.remove("mx_AppsDrawer_resizing");
// persist to localStorage
console.log("@@ _saveResizerPreferences");
localStorage.setItem(this._getStorageKey(), JSON.stringify([
this._getIdString(),
this._getAppsHash(this.state.apps),
...this.state.apps.slice(1).map((_, i) => this.resizer.forHandleAt(i).size),
]));
},
@ -121,26 +122,39 @@ export default class AppsDrawer extends React.Component {
_getStorageKey = () => `mx_apps_drawer-${this.props.room.roomId}`;
_getIdString = () => this.state.apps.map(app => app.id).join("~");
_getAppsHash = (apps) => apps.map(app => app.id).join("~");
_loadResizerPreferences = () => { // TODO call this when changing pinned apps
componentDidUpdate(prevProps, prevState) {
if (this._getAppsHash(this.state.apps) !== this._getAppsHash(prevState.apps)) {
this._loadResizerPreferences();
}
}
_loadResizerPreferences = () => {
console.log("@@ _loadResizerPreferences");
try {
const [idString, ...sizes] = JSON.parse(localStorage.getItem(this._getStorageKey()));
// format: [idString: string, ...percentages: string];
if (this._getIdString() !== idString) return;
sizes.forEach((size, i) => {
const distributor = this.resizer.forHandleAt(i);
distributor.size = size;
distributor.finish();
});
// TODO determine the exact behaviour we want for layout changing when pinning/unpinning
if (this._getAppsHash() === idString || true) {
sizes.forEach((size, i) => {
const distributor = this.resizer.forHandleAt(i);
if (distributor) {
distributor.size = size;
distributor.finish();
}
});
return;
}
} catch (e) {
console.error(e);
this.state.apps.slice(1).forEach((_, i) => {
const distributor = this.resizer.forHandleAt(i);
distributor.item.clearSize();
distributor.finish();
});
// this is expected
}
if (this.state.apps) {
console.log("@@ full relaxation");
const distributors = this.resizer.getDistributors();
distributors.forEach(d => d.item.clearSize());
distributors.forEach(d => d.finish());
}
};
@ -162,12 +176,12 @@ export default class AppsDrawer extends React.Component {
}
};
_getApps = () => WidgetStore.instance.getApps(this.props.room, true);
_getApps = () => WidgetStore.instance.getPinnedApps(this.props.room.roomId);
_updateApps = () => {
this.setState({
apps: this._getApps(),
}, this._loadResizerPreferences);
});
};
_launchManageIntegrations() {

View file

@ -163,16 +163,20 @@ export default class Resizer<C extends IConfig = IConfig> {
};
private onResize = throttle(() => {
const distributors = this.getResizeHandles().map(handle => {
const {distributor} = this.createSizerAndDistributor(<HTMLDivElement>handle);
return distributor;
});
const distributors = this.getDistributors();
// relax all items if they had any overconstrained flexboxes
distributors.forEach(d => d.start());
distributors.forEach(d => d.finish());
}, 100, {trailing: true, leading: true});
public getDistributors = () => {
return this.getResizeHandles().map(handle => {
const {distributor} = this.createSizerAndDistributor(<HTMLDivElement>handle);
return distributor;
});
};
private createSizerAndDistributor(
resizeHandle: HTMLDivElement,
): {sizer: Sizer, distributor: FixedDistributor<any>} {
@ -186,6 +190,7 @@ export default class Resizer<C extends IConfig = IConfig> {
}
private getResizeHandles() {
if (!this.container.children) return [];
return Array.from(this.container.children).filter(el => {
return this.isResizeHandle(<HTMLElement>el);
}) as HTMLElement[];

View file

@ -22,6 +22,7 @@ import { AsyncStoreWithClient } from "./AsyncStoreWithClient";
import defaultDispatcher from "../dispatcher/dispatcher";
import SettingsStore from "../settings/SettingsStore";
import WidgetEchoStore from "../stores/WidgetEchoStore";
import RoomViewStore from "../stores/RoomViewStore";
import ActiveWidgetStore from "../stores/ActiveWidgetStore";
import WidgetUtils from "../utils/WidgetUtils";
import {SettingLevel} from "../settings/SettingLevel";
@ -48,11 +49,6 @@ interface IRoomWidgets {
export const MAX_PINNED = 3;
// TODO change order to be order that they were pinned
// TODO HARD cap at 3, truncating if needed
// TODO call finish more proactively to lock things in
// TODO auto-open the appsDrawer for the room when widgets get pinned
// TODO consolidate WidgetEchoStore into this
// TODO consolidate ActiveWidgetStore into this
export default class WidgetStore extends AsyncStoreWithClient<IState> {
@ -75,7 +71,7 @@ export default class WidgetStore extends AsyncStoreWithClient<IState> {
private initRoom(roomId: string) {
if (!this.roomMap.has(roomId)) {
this.roomMap.set(roomId, {
pinned: {},
pinned: {}, // ordered
widgets: [],
});
}
@ -163,25 +159,24 @@ export default class WidgetStore extends AsyncStoreWithClient<IState> {
public isPinned(widgetId: string) {
const roomId = this.getRoomId(widgetId);
const roomInfo = this.getRoom(roomId);
let pinned = roomInfo && roomInfo.pinned[widgetId];
// Jitsi widgets should be pinned by default
const widget = this.widgetMap.get(widgetId);
if (pinned === undefined && WidgetType.JITSI.matches(widget?.type)) pinned = true;
return pinned;
return !!this.getPinnedApps(roomId).find(w => w.id === widgetId);
}
public canPin(widgetId: string) {
const roomId = this.getRoomId(widgetId);
const roomInfo = this.getRoom(roomId);
return roomInfo && Object.keys(roomInfo.pinned).filter(k => {
return roomInfo.pinned[k] && roomInfo.widgets.some(app => app.id === k);
}).length < MAX_PINNED;
return this.getPinnedApps(roomId).length < MAX_PINNED;
}
public pinWidget(widgetId: string) {
this.setPinned(widgetId, true);
// Show the apps drawer upon the user pinning a widget
if (RoomViewStore.getRoomId() === this.getRoomId(widgetId)) {
defaultDispatcher.dispatch({
action: "appsDrawer",
show: true,
})
}
}
public unpinWidget(widgetId: string) {
@ -192,6 +187,10 @@ export default class WidgetStore extends AsyncStoreWithClient<IState> {
const roomId = this.getRoomId(widgetId);
const roomInfo = this.getRoom(roomId);
if (!roomInfo) return;
if (roomInfo.pinned[widgetId] === false && value) {
// delete this before write to maintain the correct object insertion order
delete roomInfo.pinned[widgetId];
}
roomInfo.pinned[widgetId] = value;
// Clean up the pinned record
@ -206,13 +205,30 @@ export default class WidgetStore extends AsyncStoreWithClient<IState> {
this.emit(UPDATE_EVENT);
}
public getApps(room: Room, pinned?: boolean): IApp[] {
const roomInfo = this.getRoom(room.roomId);
public getPinnedApps(roomId): IApp[] {
// returns the apps in the order they were pinned with, up to the maximum
const roomInfo = this.getRoom(roomId);
if (!roomInfo) return [];
if (pinned) {
return roomInfo.widgets.filter(app => this.isPinned(app.id));
// Show Jitsi widgets even if the user already had the maximum pinned, instead of their latest pinned,
// except if the user already explicitly unpinned the Jitsi widget
const priorityWidget = roomInfo.widgets.find(widget => {
return roomInfo.pinned[widget.id] === undefined && WidgetType.JITSI.matches(widget.type);
});
const order = Object.keys(roomInfo.pinned).filter(k => roomInfo.pinned[k]);
let apps = order.map(wId => this.widgetMap.get(wId)).filter(Boolean);
apps = apps.slice(0, priorityWidget ? MAX_PINNED - 1 : MAX_PINNED);
if (priorityWidget) {
apps.push(priorityWidget);
}
return roomInfo.widgets;
return apps;
}
public getApps(roomId: string): IApp[] {
const roomInfo = this.getRoom(roomId);
return roomInfo?.widgets || [];
}
public doesRoomHaveConference(room: Room): boolean {