mirror of
https://github.com/matrix-org/matrix-spec
synced 2026-03-24 03:54:10 +01:00
Merge branch 'main' into no-canonifyurls
This commit is contained in:
commit
1bc409e222
235
assets/js/toc.js
235
assets/js/toc.js
|
|
@ -15,18 +15,122 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Set a new ToC entry.
|
Only call the given function once every 250 milliseconds to avoid impacting
|
||||||
Clear any previously highlighted ToC items, set the new one,
|
the performance of the browser.
|
||||||
and adjust the ToC scroll position.
|
Source: https://remysharp.com/2010/07/21/throttling-function-calls
|
||||||
*/
|
*/
|
||||||
function setTocEntry(newEntry) {
|
function throttle(fn) {
|
||||||
const activeEntries = document.querySelectorAll("#toc a.active");
|
const threshold = 250;
|
||||||
|
let last = null;
|
||||||
|
let deferTimer = null;
|
||||||
|
|
||||||
|
return function (...args) {
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
if (last && now < last + threshold) {
|
||||||
|
// Hold on to it.
|
||||||
|
clearTimeout(deferTimer);
|
||||||
|
deferTimer = setTimeout(() => {
|
||||||
|
last = now;
|
||||||
|
fn.apply(this, args);
|
||||||
|
}, threshold);
|
||||||
|
} else {
|
||||||
|
last = now;
|
||||||
|
fn.apply(this, args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Get the list of headings that appear in the ToC.
|
||||||
|
This is not as simple as querying all the headings in the content, because
|
||||||
|
some headings are not rendered in the ToC (e.g. in the endpoint definitions).
|
||||||
|
*/
|
||||||
|
function getHeadings() {
|
||||||
|
let headings = [];
|
||||||
|
|
||||||
|
// First get the anchors in the ToC.
|
||||||
|
const toc_anchors = document.querySelectorAll("#toc nav a");
|
||||||
|
|
||||||
|
for (const anchor of toc_anchors) {
|
||||||
|
// Then get the heading from its selector in the anchor's href.
|
||||||
|
const selector = anchor.getAttribute("href");
|
||||||
|
if (!selector) {
|
||||||
|
console.error("Got ToC anchor without href");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const heading = document.querySelector(selector);
|
||||||
|
if (!heading) {
|
||||||
|
console.error("Heading not found for selector:", selector);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
headings.push(heading);
|
||||||
|
}
|
||||||
|
|
||||||
|
return headings;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Get the heading of the text visible at the top of the viewport.
|
||||||
|
This is the first heading above or at the top of the viewport.
|
||||||
|
*/
|
||||||
|
function getCurrentHeading(headings, headerOffset) {
|
||||||
|
const scrollTop = document.documentElement.scrollTop;
|
||||||
|
let prevHeading = null;
|
||||||
|
let currentHeading = null;
|
||||||
|
let index = 0;
|
||||||
|
|
||||||
|
for (const heading of headings) {
|
||||||
|
// Compute the position compared to the viewport.
|
||||||
|
const rect = heading.getBoundingClientRect();
|
||||||
|
|
||||||
|
if (rect.top >= headerOffset && rect.top <= headerOffset + 30) {
|
||||||
|
// This heading is at the top of the viewport, this is the current heading.
|
||||||
|
currentHeading = heading;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (rect.top >= headerOffset) {
|
||||||
|
// This is in or below the viewport, the current heading should be the
|
||||||
|
// previous one.
|
||||||
|
if (prevHeading) {
|
||||||
|
currentHeading = prevHeading;
|
||||||
|
} else {
|
||||||
|
// The first heading does not have a prevHeading.
|
||||||
|
currentHeading = heading;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
prevHeading = heading;
|
||||||
|
index += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return currentHeading;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Select the ToC entry that points to the given ID.
|
||||||
|
Clear any previously highlighted ToC items, select the new one,
|
||||||
|
and adjust the ToC scroll position.
|
||||||
|
*/
|
||||||
|
function selectTocEntry(id) {
|
||||||
|
// Deselect previously selected entries.
|
||||||
|
const activeEntries = document.querySelectorAll("#toc nav a.active");
|
||||||
for (const activeEntry of activeEntries) {
|
for (const activeEntry of activeEntries) {
|
||||||
activeEntry.classList.remove('active');
|
activeEntry.classList.remove('active');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Find the new entry and select it.
|
||||||
|
const newEntry = document.querySelector(`#toc nav a[href="#${id}"]`);
|
||||||
|
if (!newEntry) {
|
||||||
|
console.error("ToC entry not found for ID:", id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
newEntry.classList.add('active');
|
newEntry.classList.add('active');
|
||||||
// don't scroll the sidebar nav if the main content is not scrolled
|
|
||||||
|
// Don't scroll the sidebar nav if the main content is not scrolled
|
||||||
const nav = document.querySelector("#td-section-nav");
|
const nav = document.querySelector("#td-section-nav");
|
||||||
const content = document.querySelector("html");
|
const content = document.querySelector("html");
|
||||||
if (content.scrollTop !== 0) {
|
if (content.scrollTop !== 0) {
|
||||||
|
|
@ -37,115 +141,24 @@ function setTocEntry(newEntry) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Test whether a node is in the viewport
|
Track when the view is scrolled, and use this to update the highlight for the
|
||||||
*/
|
corresponding ToC entry.
|
||||||
function isInViewport(node) {
|
|
||||||
const rect = node.getBoundingClientRect();
|
|
||||||
return (
|
|
||||||
rect.top >= 0 &&
|
|
||||||
rect.left >= 0 &&
|
|
||||||
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
|
|
||||||
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
The callback we pass to the IntersectionObserver constructor.
|
|
||||||
|
|
||||||
Called when any of our observed nodes starts or stops intersecting
|
|
||||||
with the viewport.
|
|
||||||
*/
|
|
||||||
function handleIntersectionUpdate(entries) {
|
|
||||||
|
|
||||||
/*
|
|
||||||
Special case: If the current URL hash matches a ToC entry, and
|
|
||||||
the corresponding heading is visible in the viewport, then that is
|
|
||||||
made the current ToC entry, and we don't even look at the intersection
|
|
||||||
observer data.
|
|
||||||
This means that if the user has clicked on a ToC entry,
|
|
||||||
we won't unselect it through the intersection observer.
|
|
||||||
*/
|
|
||||||
const hash = document.location.hash;
|
|
||||||
if (hash) {
|
|
||||||
let tocEntryForHash = document.querySelector(`nav li a[href="${hash}"]`);
|
|
||||||
// if the hash isn't a direct match for a ToC item, check the data attributes
|
|
||||||
if (!tocEntryForHash) {
|
|
||||||
const fragment = hash.substring(1);
|
|
||||||
tocEntryForHash = document.querySelector(`nav li a[data-${fragment}]`);
|
|
||||||
}
|
|
||||||
if (tocEntryForHash) {
|
|
||||||
const headingForHash = document.querySelector(hash);
|
|
||||||
if (headingForHash && isInViewport(headingForHash)) {
|
|
||||||
setTocEntry(tocEntryForHash);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let newEntry = null;
|
|
||||||
|
|
||||||
for (const entry of entries) {
|
|
||||||
if (entry.intersectionRatio > 0) {
|
|
||||||
const heading = entry.target;
|
|
||||||
/*
|
|
||||||
This sidebar nav consists of two sections:
|
|
||||||
* at the top, a sitenav containing links to other pages
|
|
||||||
* under that, the ToC for the current page
|
|
||||||
|
|
||||||
Since the sidebar scrolls to match the document position,
|
|
||||||
the sitenav will tend to scroll off the screen.
|
|
||||||
|
|
||||||
If the user has scrolled up to (or near) the top of the page,
|
|
||||||
we want to show the sitenav so.
|
|
||||||
|
|
||||||
So: if the H1 (title) for the current page has started
|
|
||||||
intersecting, then always scroll the sidebar back to the top.
|
|
||||||
*/
|
|
||||||
if (heading.tagName === "H1" && heading.parentNode.tagName === "DIV") {
|
|
||||||
const nav = document.querySelector("#td-section-nav");
|
|
||||||
nav.scrollTop = 0;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
/*
|
|
||||||
Otherwise, get the ToC entry for the first entry that
|
|
||||||
entered the viewport, if there was one.
|
|
||||||
*/
|
|
||||||
const id = entry.target.getAttribute('id');
|
|
||||||
let tocEntry = document.querySelector(`nav li a[href="#${id}"]`);
|
|
||||||
// if the id isn't a direct match for a ToC item,
|
|
||||||
// check the ToC entry's `data-*` attributes
|
|
||||||
if (!tocEntry) {
|
|
||||||
tocEntry = document.querySelector(`nav li a[data-${id}]`);
|
|
||||||
}
|
|
||||||
if (tocEntry && !newEntry) {
|
|
||||||
newEntry = tocEntry;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newEntry) {
|
|
||||||
setTocEntry(newEntry);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
Track when headings enter the viewport, and use this to update the highlight
|
|
||||||
for the corresponding ToC entry.
|
|
||||||
*/
|
*/
|
||||||
window.addEventListener('DOMContentLoaded', () => {
|
window.addEventListener('DOMContentLoaded', () => {
|
||||||
|
// Part of the viewport is below the header so we should take it into account.
|
||||||
|
const headerOffset = document.querySelector("body > header > nav").clientHeight;
|
||||||
|
const headings = getHeadings();
|
||||||
|
|
||||||
const toc = document.querySelector("#toc");
|
const onScroll = throttle((_e) => {
|
||||||
toc.addEventListener("click", event => {
|
// Update the ToC.
|
||||||
if (event.target.tagName === "A") {
|
let heading = getCurrentHeading(headings, headerOffset);
|
||||||
setTocEntry(event.target);
|
selectTocEntry(heading.id);
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const observer = new IntersectionObserver(handleIntersectionUpdate);
|
// Initialize the state of the ToC.
|
||||||
|
onScroll();
|
||||||
document.querySelectorAll("h1, h2, h3, h4, h5, h6").forEach((section) => {
|
|
||||||
observer.observe(section);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
// Listen to scroll and resizing changes.
|
||||||
|
document.addEventListener('scroll', onScroll, false);
|
||||||
|
document.addEventListener('resize', onScroll, false);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
Correct OpenAPI specification for query parameters to `GET /_matrix/client/v3/thirdparty/location/{protocol}` endpoint.
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
Clarify formats of string types.
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
Clarify formats of string types.
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
Clarify formats of string types.
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
Clarify that the async upload endpoint will return 404 in some cases.
|
||||||
1
changelogs/internal/newsfragments/1991.clarification
Normal file
1
changelogs/internal/newsfragments/1991.clarification
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Improve the JS script to highlight the current ToC entry.
|
||||||
1
changelogs/push_gateway/newsfragments/1974.clarification
Normal file
1
changelogs/push_gateway/newsfragments/1974.clarification
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
The path of HTTP pusher URLs is fixed to `/_matrix/push/v1/notify`.
|
||||||
|
|
@ -126,6 +126,25 @@ paths:
|
||||||
"errcode": "M_FORBIDDEN",
|
"errcode": "M_FORBIDDEN",
|
||||||
"error": "Cannot upload this content"
|
"error": "Cannot upload this content"
|
||||||
}
|
}
|
||||||
|
"404":
|
||||||
|
description: |-
|
||||||
|
The user has provided an invalid MXC ID. Some reasons for this error include:
|
||||||
|
|
||||||
|
- The MXC ID was not created with [POST /_matrix/media/v1/create](/client-server-api/#post_matrixmediav1create).
|
||||||
|
- The MXC ID has expired.
|
||||||
|
|
||||||
|
A [standard error response](/client-server-api/#standard-error-response)
|
||||||
|
will be returned with the `errcode` `M_NOT_FOUND`.
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: definitions/errors/error.yaml
|
||||||
|
examples:
|
||||||
|
response:
|
||||||
|
value: {
|
||||||
|
"errcode": "M_NOT_FOUND",
|
||||||
|
"error": "Unknown media ID"
|
||||||
|
}
|
||||||
"409":
|
"409":
|
||||||
description: |-
|
description: |-
|
||||||
The endpoint was called with a media ID that already has content. A
|
The endpoint was called with a media ID that already has content. A
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,7 @@ properties:
|
||||||
arbitrary string with no specified maximum length.
|
arbitrary string with no specified maximum length.
|
||||||
url:
|
url:
|
||||||
type: string
|
type: string
|
||||||
|
format: uri
|
||||||
description: |
|
description: |
|
||||||
A link to the text of this document, in the appropriate
|
A link to the text of this document, in the appropriate
|
||||||
language. MUST be a valid URI with scheme `https://` or
|
language. MUST be a valid URI with scheme `https://` or
|
||||||
|
|
|
||||||
|
|
@ -172,6 +172,8 @@ paths:
|
||||||
properties:
|
properties:
|
||||||
user_id:
|
user_id:
|
||||||
type: string
|
type: string
|
||||||
|
format: mx-user-id
|
||||||
|
pattern: "^@"
|
||||||
description: The fully-qualified Matrix ID for the account.
|
description: The fully-qualified Matrix ID for the account.
|
||||||
access_token:
|
access_token:
|
||||||
type: string
|
type: string
|
||||||
|
|
@ -197,6 +199,7 @@ paths:
|
||||||
x-addedInMatrixVersion: "1.3"
|
x-addedInMatrixVersion: "1.3"
|
||||||
home_server:
|
home_server:
|
||||||
type: string
|
type: string
|
||||||
|
format: mx-server-name
|
||||||
deprecated: true
|
deprecated: true
|
||||||
description: |-
|
description: |-
|
||||||
The server_name of the homeserver on which the account has
|
The server_name of the homeserver on which the account has
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,8 @@ paths:
|
||||||
properties:
|
properties:
|
||||||
matrix_id:
|
matrix_id:
|
||||||
type: string
|
type: string
|
||||||
|
format: mx-user-id
|
||||||
|
pattern: "^@"
|
||||||
description: |-
|
description: |-
|
||||||
A [Matrix User ID](/appendices/#user-identifiers)
|
A [Matrix User ID](/appendices/#user-identifiers)
|
||||||
representing the administrator.
|
representing the administrator.
|
||||||
|
|
@ -66,6 +68,7 @@ paths:
|
||||||
required.
|
required.
|
||||||
email_address:
|
email_address:
|
||||||
type: string
|
type: string
|
||||||
|
format: email
|
||||||
description: |-
|
description: |-
|
||||||
An email address to reach the administrator.
|
An email address to reach the administrator.
|
||||||
|
|
||||||
|
|
@ -95,6 +98,7 @@ paths:
|
||||||
}
|
}
|
||||||
support_page:
|
support_page:
|
||||||
type: string
|
type: string
|
||||||
|
format: uri
|
||||||
description: |-
|
description: |-
|
||||||
The URL of a page to give users help specific to the
|
The URL of a page to give users help specific to the
|
||||||
homeserver, like extra login/registration steps.
|
homeserver, like extra login/registration steps.
|
||||||
|
|
|
||||||
|
|
@ -98,12 +98,14 @@ paths:
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
- in: query
|
- in: query
|
||||||
name: searchFields
|
name: fields
|
||||||
description: |-
|
description: |-
|
||||||
One or more custom fields to help identify the third-party
|
One or more custom fields to help identify the third-party
|
||||||
location.
|
location.
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: object
|
||||||
|
additionalProperties:
|
||||||
|
type: string
|
||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
description: At least one portal room was found.
|
description: At least one portal room was found.
|
||||||
|
|
|
||||||
|
|
@ -35,9 +35,9 @@ paths:
|
||||||
updating counts of unread notifications should be idempotent and
|
updating counts of unread notifications should be idempotent and
|
||||||
therefore do not require duplicate suppression.
|
therefore do not require duplicate suppression.
|
||||||
|
|
||||||
Notifications are sent to the URL configured when the pusher is created.
|
Clients interested in receiving notifications via this endpoint MUST
|
||||||
This means that the HTTP path may be different depending on the push
|
configure its full URI when creating the associated pusher via
|
||||||
gateway.
|
[`/_matrix/client/v3/pushers/set`](/client-server-api/#post_matrixclientv3pushersset).
|
||||||
operationId: notify
|
operationId: notify
|
||||||
requestBody:
|
requestBody:
|
||||||
content:
|
content:
|
||||||
|
|
|
||||||
|
|
@ -64,3 +64,7 @@ mx-mxc-uri:
|
||||||
uri:
|
uri:
|
||||||
title: URI
|
title: URI
|
||||||
url: https://datatracker.ietf.org/doc/html/rfc3986
|
url: https://datatracker.ietf.org/doc/html/rfc3986
|
||||||
|
|
||||||
|
email:
|
||||||
|
title: Email Address
|
||||||
|
url: https://datatracker.ietf.org/doc/html/rfc5321#section-4.1.2
|
||||||
|
|
|
||||||
|
|
@ -2,15 +2,8 @@
|
||||||
|
|
||||||
This template is included at the end of each page's `<body>`.
|
This template is included at the end of each page's `<body>`.
|
||||||
|
|
||||||
We're using it here to:
|
We're using it here to highlight and scroll the ToC in the sidebar to match
|
||||||
|
the place we are at in the document.
|
||||||
1) include the JS that generates the table of contents. It would be better
|
|
||||||
to generate the table of contents as part of the Hugo build process, but
|
|
||||||
that doesn't work nicely with the way we want to author client-server modules
|
|
||||||
as separate files.
|
|
||||||
|
|
||||||
2) highlight and scroll the ToC in the sidebar to match the place we are at
|
|
||||||
in the document.
|
|
||||||
|
|
||||||
*/}}
|
*/}}
|
||||||
{{ $toc := resources.Get "js/toc.js" -}}
|
{{ $toc := resources.Get "js/toc.js" -}}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue