1
0
mirror of https://github.com/element-hq/element-web.git synced 2026-10-23 11:09:01 +03:00
Files
element-web/apps/web/res/css/views/dialogs/_InviteDialog.pcss
hayyaksi 5fddfd24e9 Stop blaming permissions when the homeserver refuses an invite (#34590)
* Say why the homeserver refused an invite instead of blaming permissions

Every M_FORBIDDEN from an invite was reported as "You do not have
permission to invite people to this room", and the rest of the batch was
abandoned on the spot. That is only one of the reasons a server has for
refusing: the invitee may have invites switched off, or their homeserver
may not accept ours. Someone who did have the power to invite was told
they did not, and given nothing to act on.

The claim is now checked before it is made. If the room says we cannot
invite, the message and the abandoned batch are exactly as they were,
because that will hold for everyone in the batch. If we can invite, the
server's own reason is shown instead and the remaining invites are still
attempted, since a refusal aimed at one person says nothing about the
next.

The unfederated case comes out of the same branch unchanged. It was
reached through a nested conditional that had to spell itself out twice,
once for rooms and once for spaces, and reads better checked first.

Tests: a refusal we are allowed to send through now surfaces the server's
text and lets the batch continue, while a genuine lack of power still
gives the permission message and stops.

* Drop the homeserver's own wording from the invite refusal message

The reason a server gives for an M_FORBIDDEN comes back in English whatever
language the client is running in, so showing it leaves the user reading a
sentence they may not understand inside an otherwise translated dialog.
Nothing else in the app passes server text through to the UI.

The message now says only that the invite was refused, which is fully
translatable. The reason is not lost: the catch block already logs the whole
error, so it stays in the console and in a rageshake, where an untranslated
string does no harm and is where anyone diagnosing the refusal would look
anyway.

The rest of the change stands. Permissions are blamed only when the room says
we genuinely cannot invite, and a refusal aimed at one invitee no longer
abandons the rest of the batch.

* Reword the dialog that lists the invites a batch could not send

The dialog headed "Some invites couldn't be sent" goes on to say "We sent the others, but the below
people couldn't be invited to <room>". "Some" reads as more than one, though a batch of two can fail
on exactly one; "below" names the list by where it sits on the screen rather than by what it is; and
the room name is repeated back from the very room the user opened the invite dialog in.

The heading now reports what happened rather than what was possible, the list is introduced as the
following people, and the contractions are spelled out. The room name goes with them: nothing reaches
this dialog except invites the user has just sent into a room they are already looking at.

Dropping the name drops the RoomName substitution too, which is why the key is renamed rather than
reworded in place. The forty translations still carry the tag, and under the unchanged key they would
render it as literal text until each language is retranslated.

* Report a refused invite in place of the invite dialog, not on top of it

Design review asked for the failure report to take the invite dialog's place:
once the batch has run there is nothing left to go back to, because everyone who
could be invited already has been. The dialog now finishes before the report
opens, which also lets showAnyInviteErrors stop doubling as the decision about
whether to close it.

The reason line now reads as the reason rather than as a report of machinery.
The client cannot tell a person's own invite filter apart from a server that
refuses invites wholesale — both come back as a bare M_FORBIDDEN with nothing to
key off — so the wording states the effect, which is the same either way, rather
than guessing at a cause the way the permission message used to.

The report is capped at 600px, and each reason starts at the avatar's left edge
with a space above it, so it reads as a note about the person rather than as a
third line of their name.

* Keep the refusal report's own button inside its 600px box

BaseDialog puts the fixed-width class on the focus lock inside .mx_Dialog rather
than on .mx_Dialog itself, so capping the outer box alone left the body at its
old 704px and hung the OK button 144px past the right edge — measured in the
browser, not guessed. Pinning the outer box and letting the body size to it puts
them back together, and the viewport cap keeps a narrow window from overflowing.

* Stop the refusal line from pushing a name away from its avatar

Spanning the reason across both columns made it a grid item of the avatar
track, and that track was sized max-content, so the longest reason set the
avatar column's width and left a 158px hole between each avatar and its name.
Sizing the track from the avatar itself takes the reason out of that
calculation; the gap is back to the grid's own 12px and the reason still starts
at the avatar's left edge. The avatar size now has one definition instead of
being repeated as the track width.

* State each refusal once, under the people it applies to

Repeating an identical sentence under every name read as three failures with
three causes rather than one cause with three people behind it, and the eye had
to compare the lines to find out which. The failures are now grouped by the text
they would have printed, in the order the batch hit them, and each group says
its reason once beneath its own list.

Grouping also takes the reason out of the tile, which undoes the layout work the
earlier rounds needed: the tile is back to upstream's avatar-and-name grid, so
the spanning item that was inflating the max-content avatar column is gone along
with the track override that compensated for it. Spacing now comes from the
group — 8px between people, 16px above the reason — instead of a margin on every
tile.

* Play down the reason an invite could not be sent

The reason sat under the people it applies to in alert red, at the size of their names, which reads
as something gone wrong that the reader must act on. It is neither: nobody can do anything about a
homeserver turning an invite down, and the invites that did go out are unaffected. It now takes the
size and colour of the tertiary lines around it, so it explains without competing with the names
above it.

The reasons themselves repeated a subject the list had already given. Each one sits directly under
the person it describes, so "User is already in the room" said "user" twice over; the already-invited
and space wordings carried the same redundancy and lose it too. "They are not accepting invitations"
becomes "Not accepting invites", in the terser register the app already uses for a state the reader
cannot change.

* Keep the refusal reason in alert red

The reason was toned down to tertiary size and colour on the assumption that a homeserver turning an
invite down is informational. Review settled the opposite: the layout americanrefugee approved states
the reason once beneath the people it applies to, and at that position it is the only thing telling
the reader those invites did not happen. Tertiary grey at 12px reads as a caption on the list above
it rather than as the outcome, so it goes back to the alert red and the size of the names it explains.

The wording simplifications stay. Dropping the redundant subject was a separate ask and does not
depend on how the line is styled.

* Put the refusal reason above the people it applies to

The reason trailed the list it explains, so a reader met the names first and had to look back up once the
explanation arrived. Leading with it makes the line a heading for its group, and the 8px that had held it
clear of the names above now holds it clear of the names below.

A heading takes a colon rather than a full stop, but the reason is a translated string that the
single-invite failure dialog, the /invite command and the user-info invite button each show on its own,
where a trailing colon would dangle. The colon is appended where the report renders it instead, which in
turn means every reason has to arrive without a terminal stop: leave one on the wordier reasons this
branch did not introduce and they read ".:" as soon as two differently-caused failures land in the same
report. The four "already in the room" reasons line up with their existing translations again by losing
it, since upstream's English never carried one. The longer sentences will show ".:" in the locales that
have already translated them until Localazy picks the change up, which is the cheaper blemish; renaming
those keys to dodge it would throw away thirty-six translations apiece.

The alert red stays. TemoCrevlis read the line as informational and asked for tertiary grey; review
settled it the other way, on the grounds that it is the only thing on screen saying why those invites did
not happen, and red is the likelier to be read.

* Style the invite error dialog with Compound tokens

The refusal reason and the summary heading above it were still reaching
for the legacy $font-15px, $alert and $secondary-content Sass variables.
Each of those resolves to a Compound token one-for-one in every theme, so
naming the token directly costs nothing at render time and stops the
dialog depending on the compatibility layer.

The heading collapses into the --cpd-font-body-md-regular shorthand,
which already carries the regular weight and Compound's 1.5 line height.
That trades the hand-written 24px leading for 22.5px, the value Compound
intends for body-md text, rather than keeping a legacy variable alive
just to preserve a leading nobody chose deliberately.
2026-08-26 09:03:30 +00:00

329 lines
7.3 KiB
Plaintext

/*
Copyright 2024 New Vector Ltd.
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
.mx_InviteDialog_flexWrapper .mx_Dialog {
display: flex;
flex-direction: column;
}
.mx_InviteDialog_transferWrapper .mx_Dialog {
padding-bottom: var(--cpd-space-4x);
}
.mx_InviteDialog_addressBar {
display: flex;
flex-direction: row;
/* Right margin for the design. We could apply this to the whole dialog, but then the scrollbar */
/* for the user section gets weird. */
margin: var(--cpd-space-2x) 45px 0 0;
.mx_InviteDialog_editor {
flex: 1;
margin-left: var(--cpd-space-0-5x);
}
.mx_InviteDialog_goButton {
min-width: 86px;
margin-inline-start: 10px;
height: 41px;
line-height: $font-25px;
}
}
.mx_InviteDialog_section {
padding-bottom: var(--cpd-space-1x);
.mx_InviteDialog_section_showMore {
margin: 7px 18px;
display: block;
}
}
.mx_InviteDialog_section_hidden_suggestions_disclaimer {
padding: var(--cpd-space-2x) 0 var(--cpd-space-4x) 0;
font: var(--cpd-font-body-md-regular);
> span {
color: $primary-content;
font-weight: var(--cpd-font-weight-semibold);
}
> p {
margin: 0;
}
}
.mx_InviteDialog_footer {
border-top: 1px solid $input-border-color;
> h3 {
margin: var(--cpd-space-3x) 0;
font-size: $font-12px;
color: $muted-fg-color;
font-weight: bold;
text-transform: uppercase;
}
.mx_CopyableText.mx_CopyableText_border {
width: unset; /* full width */
margin-bottom: 0;
> a {
text-decoration: none;
flex-shrink: 1;
overflow: hidden;
text-overflow: ellipsis;
}
}
}
.mx_InviteDialog_other {
/* Prevent the dialog from jumping around randomly when elements change. */
display: flex;
flex-direction: column;
height: 600px;
overflow: hidden;
h2 {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
white-space: pre-wrap;
overflow: hidden;
}
.mx_InviteDialog_addressBar {
margin-inline-end: 0;
}
.mx_InviteDialog_userSections {
flex-grow: 1;
padding-inline-end: 0;
display: flex;
flex-direction: column;
margin-top: var(--cpd-space-3x);
gap: var(--cpd-space-3x);
.mx_InviteDialog_section {
padding-bottom: 0;
}
}
}
.mx_InviteDialog_content {
display: flex;
flex-direction: column;
flex-grow: 1;
overflow: hidden;
.mx_InviteProgressBody {
margin-top: var(--cpd-space-12x);
}
}
.mx_InviteDialog_transfer {
width: auto;
.mx_InviteDialog_content {
width: 496px;
height: 430px;
overflow: visible;
.mx_TabbedView {
display: flex;
flex-direction: column;
flex-shrink: 1;
flex-grow: 1;
min-height: 0;
.mx_TabbedView_tabPanel {
flex-direction: column;
.mx_TabbedView_tabPanelContent {
display: flex;
flex-direction: column;
}
}
}
}
.mx_InviteDialog_addressBar {
margin-top: var(--cpd-space-2x);
}
input[type="checkbox"] {
margin-inline-end: var(--cpd-space-2x);
}
}
.mx_InviteDialog_userSections {
overflow-y: auto;
padding: 0 45px var(--cpd-space-1x) 0;
}
.mx_InviteDialog_helpText {
margin: 0;
}
.mx_InviteDialog_dialPad {
width: 224px;
margin-top: var(--cpd-space-4x);
margin-inline: auto;
.mx_InviteDialog_dialPadField {
border-top: 0;
border-inline: 0;
border-radius: 0;
margin-top: 0;
border-color: $quaternary-content;
&:focus-within {
border-color: $accent;
}
input {
font-size: 18px;
font-weight: var(--cpd-font-weight-semibold);
padding-top: 0;
}
.mx_Field_postfix {
/* Remove border separator between postfix and field content */
border-left: none;
}
}
.mx_DialPad {
row-gap: var(--cpd-space-4x);
column-gap: 48px;
margin-inline: auto;
}
}
.mx_InviteDialog_transferConsultConnect {
padding-top: var(--cpd-space-4x);
/* This wants a drop shadow the full width of the dialog, so use negative margin to make it full width,
* then compensate with padding
*/
padding-inline: var(--cpd-space-6x);
margin-inline: calc(-1 * var(--cpd-space-6x));
border-top: 1px solid $quinary-content;
display: flex;
flex-direction: row;
align-items: center;
}
.mx_InviteDialog_transferConsultConnect_pushRight {
margin-inline-start: auto;
}
.mx_InviteDialog_tile {
cursor: pointer;
display: grid;
gap: var(--cpd-space-2x) var(--cpd-space-3x);
align-items: center;
&.mx_InviteDialog_tile--inviterError {
grid-template-columns: max-content auto; /* max-content = avatar width */
}
* {
vertical-align: middle;
}
.mx_InviteDialog_tile_avatarStack {
width: 36px;
height: 36px;
display: inline-block;
position: relative;
grid-row-start: 1;
grid-column-start: 1;
& > * {
position: absolute;
top: 0;
left: 0;
}
}
.mx_InviteDialog_tile_nameStack {
grid-row-start: 1;
grid-column-start: 2;
display: flex;
flex-flow: column;
align-self: center;
align-items: baseline;
gap: 2px 0;
overflow: hidden;
.mx_InviteDialog_tile_nameStack_name,
.mx_InviteDialog_tile_nameStack_userId {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 100%;
}
.mx_InviteDialog_tile_nameStack_name {
font-size: $font-15px;
font-weight: var(--cpd-font-weight-semibold);
color: $primary-content;
}
.mx_InviteDialog_tile_nameStack_userId {
font-size: $font-12px;
color: $muted-fg-color;
}
}
}
.mx_InviteDialog_multiInviterErrorWrapper .mx_Dialog {
width: 600px;
max-width: 100%;
.mx_Dialog_fixedWidth {
width: auto;
}
}
.mx_InviteDialog_multiInviterError {
> h4 {
font: var(--cpd-font-body-md-regular);
color: var(--cpd-color-text-secondary);
}
.mx_InviteDialog_multiInviterError_group {
display: flex;
flex-direction: column;
gap: var(--cpd-space-2x);
margin-bottom: var(--cpd-space-6x);
&:last-child {
margin-bottom: 0;
}
}
.mx_InviteDialog_multiInviterError_reason {
margin-bottom: var(--cpd-space-2x);
font-size: var(--cpd-font-size-body-md);
color: var(--cpd-color-text-critical-primary);
}
}
.mx_InviteDialog_identityServer {
margin-top: 1em;
}
.mx_InviteDialog_oneThreepid {
font-size: $font-12px;
margin: var(--cpd-space-2x) 0;
}