Merge branch 'develop' into widget_state_no_update_invitation_room
9
.github/workflows/cypress.yaml
vendored
@ -92,6 +92,12 @@ jobs:
|
|||||||
# # Run 4 instances in Parallel
|
# # Run 4 instances in Parallel
|
||||||
# runner: [1, 2, 3, 4]
|
# runner: [1, 2, 3, 4]
|
||||||
steps:
|
steps:
|
||||||
|
- uses: tecolicom/actions-use-apt-tools@v1
|
||||||
|
with:
|
||||||
|
# Our test suite includes some screenshot tests with unusual diacritics, which are
|
||||||
|
# supposed to be covered by STIXGeneral.
|
||||||
|
tools: fonts-stix
|
||||||
|
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
# XXX: We're checking out untrusted code in a secure context
|
# XXX: We're checking out untrusted code in a secure context
|
||||||
@ -241,7 +247,8 @@ jobs:
|
|||||||
--run-id $TESTRAIL_RUN_ID \
|
--run-id $TESTRAIL_RUN_ID \
|
||||||
--suite-id $TESTRAIL_SUITE_ID \
|
--suite-id $TESTRAIL_SUITE_ID \
|
||||||
--title "if you see this please check cypress build for run id not being provisioned" \
|
--title "if you see this please check cypress build for run id not being provisioned" \
|
||||||
-f $file
|
-f $file || true
|
||||||
|
# We want to keep uploading what we can; but don't want the failures/red marks when it fails, so we add || true above.
|
||||||
done
|
done
|
||||||
- name: Close test run
|
- name: Close test run
|
||||||
id: testrailpost
|
id: testrailpost
|
||||||
|
3
.github/workflows/netlify.yaml
vendored
@ -35,14 +35,13 @@ jobs:
|
|||||||
- name: 📥 Download artifact
|
- name: 📥 Download artifact
|
||||||
uses: dawidd6/action-download-artifact@v2
|
uses: dawidd6/action-download-artifact@v2
|
||||||
with:
|
with:
|
||||||
workflow: element-build-and-test.yaml
|
|
||||||
run_id: ${{ github.event.workflow_run.id }}
|
run_id: ${{ github.event.workflow_run.id }}
|
||||||
name: previewbuild
|
name: previewbuild
|
||||||
path: webapp
|
path: webapp
|
||||||
|
|
||||||
- name: ☁️ Deploy to Netlify
|
- name: ☁️ Deploy to Netlify
|
||||||
id: netlify
|
id: netlify
|
||||||
uses: nwtgck/actions-netlify@v1.2
|
uses: nwtgck/actions-netlify@v2.0
|
||||||
with:
|
with:
|
||||||
publish-dir: webapp
|
publish-dir: webapp
|
||||||
deploy-message: "Deploy from GitHub Actions"
|
deploy-message: "Deploy from GitHub Actions"
|
||||||
|
@ -14,3 +14,6 @@ package-lock.json
|
|||||||
yarn.lock
|
yarn.lock
|
||||||
|
|
||||||
/src/i18n/strings
|
/src/i18n/strings
|
||||||
|
|
||||||
|
# This file is owned, parsed, and generated by allchange, which doesn't comply with prettier
|
||||||
|
/CHANGELOG.md
|
||||||
|
27889
CHANGELOG.md
276
cypress/e2e/integration-manager/read_events.spec.ts
Normal file
@ -0,0 +1,276 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
|
import { SynapseInstance } from "../../plugins/synapsedocker";
|
||||||
|
import { UserCredentials } from "../../support/login";
|
||||||
|
|
||||||
|
const ROOM_NAME = "Integration Manager Test";
|
||||||
|
const USER_DISPLAY_NAME = "Alice";
|
||||||
|
|
||||||
|
const INTEGRATION_MANAGER_TOKEN = "DefinitelySecret_DoNotUseThisForReal";
|
||||||
|
const INTEGRATION_MANAGER_HTML = `
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>Fake Integration Manager</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<input type="text" id="target-room-id"/>
|
||||||
|
<input type="text" id="event-type"/>
|
||||||
|
<input type="text" id="state-key"/>
|
||||||
|
<button name="Send" id="send-action">Press to send action</button>
|
||||||
|
<button name="Close" id="close">Press to close</button>
|
||||||
|
<p id="message-response">No response</p>
|
||||||
|
<script>
|
||||||
|
document.getElementById("send-action").onclick = () => {
|
||||||
|
window.parent.postMessage(
|
||||||
|
{
|
||||||
|
action: "read_events",
|
||||||
|
room_id: document.getElementById("target-room-id").value,
|
||||||
|
type: document.getElementById("event-type").value,
|
||||||
|
state_key: JSON.parse(document.getElementById("state-key").value),
|
||||||
|
},
|
||||||
|
'*',
|
||||||
|
);
|
||||||
|
};
|
||||||
|
document.getElementById("close").onclick = () => {
|
||||||
|
window.parent.postMessage(
|
||||||
|
{
|
||||||
|
action: "close_scalar",
|
||||||
|
},
|
||||||
|
'*',
|
||||||
|
);
|
||||||
|
};
|
||||||
|
// Listen for a postmessage response
|
||||||
|
window.addEventListener("message", (event) => {
|
||||||
|
document.getElementById("message-response").innerText = JSON.stringify(event.data);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
|
||||||
|
function openIntegrationManager() {
|
||||||
|
cy.get(".mx_RightPanel_roomSummaryButton").click();
|
||||||
|
cy.get(".mx_RoomSummaryCard_appsGroup").within(() => {
|
||||||
|
cy.contains("Add widgets, bridges & bots").click();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendActionFromIntegrationManager(
|
||||||
|
integrationManagerUrl: string,
|
||||||
|
targetRoomId: string,
|
||||||
|
eventType: string,
|
||||||
|
stateKey: string | boolean,
|
||||||
|
) {
|
||||||
|
cy.accessIframe(`iframe[src*="${integrationManagerUrl}"]`).within(() => {
|
||||||
|
cy.get("#target-room-id").should("exist").type(targetRoomId);
|
||||||
|
cy.get("#event-type").should("exist").type(eventType);
|
||||||
|
cy.get("#state-key").should("exist").type(JSON.stringify(stateKey));
|
||||||
|
cy.get("#send-action").should("exist").click();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("Integration Manager: Read Events", () => {
|
||||||
|
let testUser: UserCredentials;
|
||||||
|
let synapse: SynapseInstance;
|
||||||
|
let integrationManagerUrl: string;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.serveHtmlFile(INTEGRATION_MANAGER_HTML).then((url) => {
|
||||||
|
integrationManagerUrl = url;
|
||||||
|
});
|
||||||
|
cy.startSynapse("default").then((data) => {
|
||||||
|
synapse = data;
|
||||||
|
|
||||||
|
cy.initTestUser(synapse, USER_DISPLAY_NAME, () => {
|
||||||
|
cy.window().then((win) => {
|
||||||
|
win.localStorage.setItem("mx_scalar_token", INTEGRATION_MANAGER_TOKEN);
|
||||||
|
win.localStorage.setItem(`mx_scalar_token_at_${integrationManagerUrl}`, INTEGRATION_MANAGER_TOKEN);
|
||||||
|
});
|
||||||
|
}).then((user) => {
|
||||||
|
testUser = user;
|
||||||
|
});
|
||||||
|
|
||||||
|
cy.setAccountData("m.widgets", {
|
||||||
|
"m.integration_manager": {
|
||||||
|
content: {
|
||||||
|
type: "m.integration_manager",
|
||||||
|
name: "Integration Manager",
|
||||||
|
url: integrationManagerUrl,
|
||||||
|
data: {
|
||||||
|
api_url: integrationManagerUrl,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
id: "integration-manager",
|
||||||
|
},
|
||||||
|
}).as("integrationManager");
|
||||||
|
|
||||||
|
// Succeed when checking the token is valid
|
||||||
|
cy.intercept(`${integrationManagerUrl}/account?scalar_token=${INTEGRATION_MANAGER_TOKEN}*`, (req) => {
|
||||||
|
req.continue((res) => {
|
||||||
|
return res.send(200, {
|
||||||
|
user_id: testUser.userId,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
cy.createRoom({
|
||||||
|
name: ROOM_NAME,
|
||||||
|
}).as("roomId");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
cy.stopSynapse(synapse);
|
||||||
|
cy.stopWebServers();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should read a state event by state key", () => {
|
||||||
|
cy.all([cy.get<string>("@roomId"), cy.get<{}>("@integrationManager")]).then(([roomId]) => {
|
||||||
|
cy.viewRoomByName(ROOM_NAME);
|
||||||
|
|
||||||
|
const eventType = "io.element.integrations.installations";
|
||||||
|
const eventContent = {
|
||||||
|
foo: "bar",
|
||||||
|
};
|
||||||
|
const stateKey = "state-key-123";
|
||||||
|
|
||||||
|
// Send a state event
|
||||||
|
cy.getClient()
|
||||||
|
.then(async (client) => {
|
||||||
|
return await client.sendStateEvent(roomId, eventType, eventContent, stateKey);
|
||||||
|
})
|
||||||
|
.then((event) => {
|
||||||
|
openIntegrationManager();
|
||||||
|
|
||||||
|
// Read state events
|
||||||
|
sendActionFromIntegrationManager(integrationManagerUrl, roomId, eventType, stateKey);
|
||||||
|
|
||||||
|
// Check the response
|
||||||
|
cy.accessIframe(`iframe[src*="${integrationManagerUrl}"]`).within(() => {
|
||||||
|
cy.get("#message-response")
|
||||||
|
.should("include.text", event.event_id)
|
||||||
|
.should("include.text", `"content":${JSON.stringify(eventContent)}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should read a state event with empty state key", () => {
|
||||||
|
cy.all([cy.get<string>("@roomId"), cy.get<{}>("@integrationManager")]).then(([roomId]) => {
|
||||||
|
cy.viewRoomByName(ROOM_NAME);
|
||||||
|
|
||||||
|
const eventType = "io.element.integrations.installations";
|
||||||
|
const eventContent = {
|
||||||
|
foo: "bar",
|
||||||
|
};
|
||||||
|
const stateKey = "";
|
||||||
|
|
||||||
|
// Send a state event
|
||||||
|
cy.getClient()
|
||||||
|
.then(async (client) => {
|
||||||
|
return await client.sendStateEvent(roomId, eventType, eventContent, stateKey);
|
||||||
|
})
|
||||||
|
.then((event) => {
|
||||||
|
openIntegrationManager();
|
||||||
|
|
||||||
|
// Read state events
|
||||||
|
sendActionFromIntegrationManager(integrationManagerUrl, roomId, eventType, stateKey);
|
||||||
|
|
||||||
|
// Check the response
|
||||||
|
cy.accessIframe(`iframe[src*="${integrationManagerUrl}"]`).within(() => {
|
||||||
|
cy.get("#message-response")
|
||||||
|
.should("include.text", event.event_id)
|
||||||
|
.should("include.text", `"content":${JSON.stringify(eventContent)}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should read state events with any state key", () => {
|
||||||
|
cy.all([cy.get<string>("@roomId"), cy.get<{}>("@integrationManager")]).then(([roomId]) => {
|
||||||
|
cy.viewRoomByName(ROOM_NAME);
|
||||||
|
|
||||||
|
const eventType = "io.element.integrations.installations";
|
||||||
|
|
||||||
|
const stateKey1 = "state-key-123";
|
||||||
|
const eventContent1 = {
|
||||||
|
foo1: "bar1",
|
||||||
|
};
|
||||||
|
const stateKey2 = "state-key-456";
|
||||||
|
const eventContent2 = {
|
||||||
|
foo2: "bar2",
|
||||||
|
};
|
||||||
|
const stateKey3 = "state-key-789";
|
||||||
|
const eventContent3 = {
|
||||||
|
foo3: "bar3",
|
||||||
|
};
|
||||||
|
|
||||||
|
// Send state events
|
||||||
|
cy.getClient()
|
||||||
|
.then(async (client) => {
|
||||||
|
return Promise.all([
|
||||||
|
client.sendStateEvent(roomId, eventType, eventContent1, stateKey1),
|
||||||
|
client.sendStateEvent(roomId, eventType, eventContent2, stateKey2),
|
||||||
|
client.sendStateEvent(roomId, eventType, eventContent3, stateKey3),
|
||||||
|
]);
|
||||||
|
})
|
||||||
|
.then((events) => {
|
||||||
|
openIntegrationManager();
|
||||||
|
|
||||||
|
// Read state events
|
||||||
|
sendActionFromIntegrationManager(
|
||||||
|
integrationManagerUrl,
|
||||||
|
roomId,
|
||||||
|
eventType,
|
||||||
|
true, // Any state key
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check the response
|
||||||
|
cy.accessIframe(`iframe[src*="${integrationManagerUrl}"]`).within(() => {
|
||||||
|
cy.get("#message-response")
|
||||||
|
.should("include.text", events[0].event_id)
|
||||||
|
.should("include.text", `"content":${JSON.stringify(eventContent1)}`)
|
||||||
|
.should("include.text", events[1].event_id)
|
||||||
|
.should("include.text", `"content":${JSON.stringify(eventContent2)}`)
|
||||||
|
.should("include.text", events[2].event_id)
|
||||||
|
.should("include.text", `"content":${JSON.stringify(eventContent3)}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fail to read an event type which is not allowed", () => {
|
||||||
|
cy.all([cy.get<string>("@roomId"), cy.get<{}>("@integrationManager")]).then(([roomId]) => {
|
||||||
|
cy.viewRoomByName(ROOM_NAME);
|
||||||
|
|
||||||
|
const eventType = "com.example.event";
|
||||||
|
const stateKey = "";
|
||||||
|
|
||||||
|
openIntegrationManager();
|
||||||
|
|
||||||
|
// Read state events
|
||||||
|
sendActionFromIntegrationManager(integrationManagerUrl, roomId, eventType, stateKey);
|
||||||
|
|
||||||
|
// Check the response
|
||||||
|
cy.accessIframe(`iframe[src*="${integrationManagerUrl}"]`).within(() => {
|
||||||
|
cy.get("#message-response").should("include.text", "Failed to read events");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
261
cypress/e2e/integration-manager/send_event.spec.ts
Normal file
@ -0,0 +1,261 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
|
import { SynapseInstance } from "../../plugins/synapsedocker";
|
||||||
|
import { UserCredentials } from "../../support/login";
|
||||||
|
|
||||||
|
const ROOM_NAME = "Integration Manager Test";
|
||||||
|
const USER_DISPLAY_NAME = "Alice";
|
||||||
|
|
||||||
|
const INTEGRATION_MANAGER_TOKEN = "DefinitelySecret_DoNotUseThisForReal";
|
||||||
|
const INTEGRATION_MANAGER_HTML = `
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>Fake Integration Manager</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<input type="text" id="target-room-id"/>
|
||||||
|
<input type="text" id="event-type"/>
|
||||||
|
<input type="text" id="state-key"/>
|
||||||
|
<input type="text" id="event-content"/>
|
||||||
|
<button name="Send" id="send-action">Press to send action</button>
|
||||||
|
<button name="Close" id="close">Press to close</button>
|
||||||
|
<p id="message-response">No response</p>
|
||||||
|
<script>
|
||||||
|
document.getElementById("send-action").onclick = () => {
|
||||||
|
window.parent.postMessage(
|
||||||
|
{
|
||||||
|
action: "send_event",
|
||||||
|
room_id: document.getElementById("target-room-id").value,
|
||||||
|
type: document.getElementById("event-type").value,
|
||||||
|
state_key: document.getElementById("state-key").value,
|
||||||
|
content: JSON.parse(document.getElementById("event-content").value),
|
||||||
|
},
|
||||||
|
'*',
|
||||||
|
);
|
||||||
|
};
|
||||||
|
document.getElementById("close").onclick = () => {
|
||||||
|
window.parent.postMessage(
|
||||||
|
{
|
||||||
|
action: "close_scalar",
|
||||||
|
},
|
||||||
|
'*',
|
||||||
|
);
|
||||||
|
};
|
||||||
|
// Listen for a postmessage response
|
||||||
|
window.addEventListener("message", (event) => {
|
||||||
|
document.getElementById("message-response").innerText = JSON.stringify(event.data);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
|
||||||
|
function openIntegrationManager() {
|
||||||
|
cy.get(".mx_RightPanel_roomSummaryButton").click();
|
||||||
|
cy.get(".mx_RoomSummaryCard_appsGroup").within(() => {
|
||||||
|
cy.contains("Add widgets, bridges & bots").click();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendActionFromIntegrationManager(
|
||||||
|
integrationManagerUrl: string,
|
||||||
|
targetRoomId: string,
|
||||||
|
eventType: string,
|
||||||
|
stateKey: string,
|
||||||
|
content: Record<string, unknown>,
|
||||||
|
) {
|
||||||
|
cy.accessIframe(`iframe[src*="${integrationManagerUrl}"]`).within(() => {
|
||||||
|
cy.get("#target-room-id").should("exist").type(targetRoomId);
|
||||||
|
cy.get("#event-type").should("exist").type(eventType);
|
||||||
|
if (stateKey) {
|
||||||
|
cy.get("#state-key").should("exist").type(stateKey);
|
||||||
|
}
|
||||||
|
cy.get("#event-content").should("exist").type(JSON.stringify(content), { parseSpecialCharSequences: false });
|
||||||
|
cy.get("#send-action").should("exist").click();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("Integration Manager: Send Event", () => {
|
||||||
|
let testUser: UserCredentials;
|
||||||
|
let synapse: SynapseInstance;
|
||||||
|
let integrationManagerUrl: string;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.serveHtmlFile(INTEGRATION_MANAGER_HTML).then((url) => {
|
||||||
|
integrationManagerUrl = url;
|
||||||
|
});
|
||||||
|
cy.startSynapse("default").then((data) => {
|
||||||
|
synapse = data;
|
||||||
|
|
||||||
|
cy.initTestUser(synapse, USER_DISPLAY_NAME, () => {
|
||||||
|
cy.window().then((win) => {
|
||||||
|
win.localStorage.setItem("mx_scalar_token", INTEGRATION_MANAGER_TOKEN);
|
||||||
|
win.localStorage.setItem(`mx_scalar_token_at_${integrationManagerUrl}`, INTEGRATION_MANAGER_TOKEN);
|
||||||
|
});
|
||||||
|
}).then((user) => {
|
||||||
|
testUser = user;
|
||||||
|
});
|
||||||
|
|
||||||
|
cy.setAccountData("m.widgets", {
|
||||||
|
"m.integration_manager": {
|
||||||
|
content: {
|
||||||
|
type: "m.integration_manager",
|
||||||
|
name: "Integration Manager",
|
||||||
|
url: integrationManagerUrl,
|
||||||
|
data: {
|
||||||
|
api_url: integrationManagerUrl,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
id: "integration-manager",
|
||||||
|
},
|
||||||
|
}).as("integrationManager");
|
||||||
|
|
||||||
|
// Succeed when checking the token is valid
|
||||||
|
cy.intercept(`${integrationManagerUrl}/account?scalar_token=${INTEGRATION_MANAGER_TOKEN}*`, (req) => {
|
||||||
|
req.continue((res) => {
|
||||||
|
return res.send(200, {
|
||||||
|
user_id: testUser.userId,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
cy.createRoom({
|
||||||
|
name: ROOM_NAME,
|
||||||
|
}).as("roomId");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
cy.stopSynapse(synapse);
|
||||||
|
cy.stopWebServers();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should send a state event", () => {
|
||||||
|
cy.all([cy.get<string>("@roomId"), cy.get<{}>("@integrationManager")]).then(([roomId]) => {
|
||||||
|
cy.viewRoomByName(ROOM_NAME);
|
||||||
|
|
||||||
|
openIntegrationManager();
|
||||||
|
|
||||||
|
const eventType = "io.element.integrations.installations";
|
||||||
|
const eventContent = {
|
||||||
|
foo: "bar",
|
||||||
|
};
|
||||||
|
const stateKey = "state-key-123";
|
||||||
|
|
||||||
|
// Send the event
|
||||||
|
sendActionFromIntegrationManager(integrationManagerUrl, roomId, eventType, stateKey, eventContent);
|
||||||
|
|
||||||
|
// Check the response
|
||||||
|
cy.accessIframe(`iframe[src*="${integrationManagerUrl}"]`).within(() => {
|
||||||
|
cy.get("#message-response").should("include.text", "event_id");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check the event
|
||||||
|
cy.getClient()
|
||||||
|
.then(async (client) => {
|
||||||
|
return await client.getStateEvent(roomId, eventType, stateKey);
|
||||||
|
})
|
||||||
|
.then((event) => {
|
||||||
|
expect(event).to.deep.equal(eventContent);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should send a state event with empty content", () => {
|
||||||
|
cy.all([cy.get<string>("@roomId"), cy.get<{}>("@integrationManager")]).then(([roomId]) => {
|
||||||
|
cy.viewRoomByName(ROOM_NAME);
|
||||||
|
|
||||||
|
openIntegrationManager();
|
||||||
|
|
||||||
|
const eventType = "io.element.integrations.installations";
|
||||||
|
const eventContent = {};
|
||||||
|
const stateKey = "state-key-123";
|
||||||
|
|
||||||
|
// Send the event
|
||||||
|
sendActionFromIntegrationManager(integrationManagerUrl, roomId, eventType, stateKey, eventContent);
|
||||||
|
|
||||||
|
// Check the response
|
||||||
|
cy.accessIframe(`iframe[src*="${integrationManagerUrl}"]`).within(() => {
|
||||||
|
cy.get("#message-response").should("include.text", "event_id");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check the event
|
||||||
|
cy.getClient()
|
||||||
|
.then(async (client) => {
|
||||||
|
return await client.getStateEvent(roomId, eventType, stateKey);
|
||||||
|
})
|
||||||
|
.then((event) => {
|
||||||
|
expect(event).to.be.empty;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should send a state event with empty state key", () => {
|
||||||
|
cy.all([cy.get<string>("@roomId"), cy.get<{}>("@integrationManager")]).then(([roomId]) => {
|
||||||
|
cy.viewRoomByName(ROOM_NAME);
|
||||||
|
|
||||||
|
openIntegrationManager();
|
||||||
|
|
||||||
|
const eventType = "io.element.integrations.installations";
|
||||||
|
const eventContent = {
|
||||||
|
foo: "bar",
|
||||||
|
};
|
||||||
|
const stateKey = "";
|
||||||
|
|
||||||
|
// Send the event
|
||||||
|
sendActionFromIntegrationManager(integrationManagerUrl, roomId, eventType, stateKey, eventContent);
|
||||||
|
|
||||||
|
// Check the response
|
||||||
|
cy.accessIframe(`iframe[src*="${integrationManagerUrl}"]`).within(() => {
|
||||||
|
cy.get("#message-response").should("include.text", "event_id");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check the event
|
||||||
|
cy.getClient()
|
||||||
|
.then(async (client) => {
|
||||||
|
return await client.getStateEvent(roomId, eventType, stateKey);
|
||||||
|
})
|
||||||
|
.then((event) => {
|
||||||
|
expect(event).to.deep.equal(eventContent);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fail to send an event type which is not allowed", () => {
|
||||||
|
cy.all([cy.get<string>("@roomId"), cy.get<{}>("@integrationManager")]).then(([roomId]) => {
|
||||||
|
cy.viewRoomByName(ROOM_NAME);
|
||||||
|
|
||||||
|
openIntegrationManager();
|
||||||
|
|
||||||
|
const eventType = "com.example.event";
|
||||||
|
const eventContent = {
|
||||||
|
foo: "bar",
|
||||||
|
};
|
||||||
|
const stateKey = "";
|
||||||
|
|
||||||
|
// Send the event
|
||||||
|
sendActionFromIntegrationManager(integrationManagerUrl, roomId, eventType, stateKey, eventContent);
|
||||||
|
|
||||||
|
// Check the response
|
||||||
|
cy.accessIframe(`iframe[src*="${integrationManagerUrl}"]`).within(() => {
|
||||||
|
cy.get("#message-response").should("include.text", "Failed to send event");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -77,6 +77,7 @@ describe("Polls", () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
cy.enableLabsFeature("feature_threadstable");
|
||||||
cy.window().then((win) => {
|
cy.window().then((win) => {
|
||||||
win.localStorage.setItem("mx_lhs_size", "0"); // Collapse left panel for these tests
|
win.localStorage.setItem("mx_lhs_size", "0"); // Collapse left panel for these tests
|
||||||
});
|
});
|
||||||
@ -114,7 +115,10 @@ describe("Polls", () => {
|
|||||||
|
|
||||||
const pollParams = {
|
const pollParams = {
|
||||||
title: "Does the polls feature work?",
|
title: "Does the polls feature work?",
|
||||||
options: ["Yes", "No", "Maybe"],
|
// Since we're going to take a screenshot anyways, we include some
|
||||||
|
// non-ASCII characters here to stress test the app's font config
|
||||||
|
// while we're at it.
|
||||||
|
options: ["Yes", "Noo⃐o⃑o⃩o⃪o⃫o⃬o⃭o⃮o⃯", "のらねこ Maybe?"],
|
||||||
};
|
};
|
||||||
createPoll(pollParams);
|
createPoll(pollParams);
|
||||||
|
|
||||||
|
@ -18,6 +18,7 @@ limitations under the License.
|
|||||||
|
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import { MatrixClient } from "matrix-js-sdk/src/matrix";
|
import { MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||||
|
import { Interception } from "cypress/types/net-stubbing";
|
||||||
|
|
||||||
import { SynapseInstance } from "../../plugins/synapsedocker";
|
import { SynapseInstance } from "../../plugins/synapsedocker";
|
||||||
import { SettingLevel } from "../../../src/settings/SettingLevel";
|
import { SettingLevel } from "../../../src/settings/SettingLevel";
|
||||||
@ -407,4 +408,55 @@ describe("Sliding Sync", () => {
|
|||||||
// ensure the reply-to does not disappear
|
// ensure the reply-to does not disappear
|
||||||
cy.get(".mx_ReplyPreview").should("exist");
|
cy.get(".mx_ReplyPreview").should("exist");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should send unsubscribe_rooms for every room switch", () => {
|
||||||
|
let roomAId: string;
|
||||||
|
let roomPId: string;
|
||||||
|
// create rooms and check room names are correct
|
||||||
|
cy.createRoom({ name: "Apple" })
|
||||||
|
.as("roomA")
|
||||||
|
.then((roomId) => (roomAId = roomId))
|
||||||
|
.then(() => cy.contains(".mx_RoomSublist", "Apple"));
|
||||||
|
|
||||||
|
cy.createRoom({ name: "Pineapple" })
|
||||||
|
.as("roomP")
|
||||||
|
.then((roomId) => (roomPId = roomId))
|
||||||
|
.then(() => cy.contains(".mx_RoomSublist", "Pineapple"));
|
||||||
|
cy.createRoom({ name: "Orange" })
|
||||||
|
.as("roomO")
|
||||||
|
.then(() => cy.contains(".mx_RoomSublist", "Orange"));
|
||||||
|
|
||||||
|
// Intercept all calls to /sync
|
||||||
|
cy.intercept({ method: "POST", url: "**/sync*" }).as("syncRequest");
|
||||||
|
|
||||||
|
const assertUnsubExists = (interception: Interception, subRoomId: string, unsubRoomId: string) => {
|
||||||
|
const body = interception.request.body;
|
||||||
|
// There may be a request without a txn_id, ignore it, as there won't be any subscription changes
|
||||||
|
if (body.txn_id === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
expect(body.unsubscribe_rooms).eql([unsubRoomId]);
|
||||||
|
expect(body.room_subscriptions).to.not.have.property(unsubRoomId);
|
||||||
|
expect(body.room_subscriptions).to.have.property(subRoomId);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Select the Test Room
|
||||||
|
cy.contains(".mx_RoomTile", "Apple").click();
|
||||||
|
|
||||||
|
// and wait for cypress to get the result as alias
|
||||||
|
cy.wait("@syncRequest").then((interception) => {
|
||||||
|
// This is the first switch, so no unsubscriptions yet.
|
||||||
|
assert.isObject(interception.request.body.room_subscriptions, "room_subscriptions is object");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Switch to another room
|
||||||
|
cy.contains(".mx_RoomTile", "Pineapple").click();
|
||||||
|
cy.wait("@syncRequest").then((interception) => assertUnsubExists(interception, roomPId, roomAId));
|
||||||
|
|
||||||
|
// And switch to even another room
|
||||||
|
cy.contains(".mx_RoomTile", "Apple").click();
|
||||||
|
cy.wait("@syncRequest").then((interception) => assertUnsubExists(interception, roomPId, roomAId));
|
||||||
|
|
||||||
|
// TODO: Add tests for encrypted rooms
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -77,8 +77,10 @@ describe("Spaces", () => {
|
|||||||
cy.stopSynapse(synapse);
|
cy.stopSynapse(synapse);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should allow user to create public space", () => {
|
it.only("should allow user to create public space", () => {
|
||||||
openSpaceCreateMenu().within(() => {
|
openSpaceCreateMenu();
|
||||||
|
cy.get("#mx_ContextualMenu_Container").percySnapshotElement("Space create menu");
|
||||||
|
cy.get(".mx_SpaceCreateMenu_wrapper .mx_ContextualMenu").within(() => {
|
||||||
cy.get(".mx_SpaceCreateMenuType_public").click();
|
cy.get(".mx_SpaceCreateMenuType_public").click();
|
||||||
cy.get('.mx_SpaceBasicSettings_avatarContainer input[type="file"]').selectFile(
|
cy.get('.mx_SpaceBasicSettings_avatarContainer input[type="file"]').selectFile(
|
||||||
"cypress/fixtures/riot.png",
|
"cypress/fixtures/riot.png",
|
||||||
|
@ -19,10 +19,17 @@ limitations under the License.
|
|||||||
import { SynapseInstance } from "../../plugins/synapsedocker";
|
import { SynapseInstance } from "../../plugins/synapsedocker";
|
||||||
import { MatrixClient } from "../../global";
|
import { MatrixClient } from "../../global";
|
||||||
|
|
||||||
|
function markWindowBeforeReload(): void {
|
||||||
|
// mark our window object to "know" when it gets reloaded
|
||||||
|
cy.window().then((w) => (w.beforeReload = true));
|
||||||
|
}
|
||||||
|
|
||||||
describe("Threads", () => {
|
describe("Threads", () => {
|
||||||
let synapse: SynapseInstance;
|
let synapse: SynapseInstance;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
// Default threads to ON for this spec
|
||||||
|
cy.enableLabsFeature("feature_threadstable");
|
||||||
cy.window().then((win) => {
|
cy.window().then((win) => {
|
||||||
win.localStorage.setItem("mx_lhs_size", "0"); // Collapse left panel for these tests
|
win.localStorage.setItem("mx_lhs_size", "0"); // Collapse left panel for these tests
|
||||||
});
|
});
|
||||||
@ -37,6 +44,35 @@ describe("Threads", () => {
|
|||||||
cy.stopSynapse(synapse);
|
cy.stopSynapse(synapse);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should reload when enabling threads beta", () => {
|
||||||
|
markWindowBeforeReload();
|
||||||
|
|
||||||
|
// Turn off
|
||||||
|
cy.openUserSettings("Labs").within(() => {
|
||||||
|
// initially the new property is there
|
||||||
|
cy.window().should("have.prop", "beforeReload", true);
|
||||||
|
|
||||||
|
cy.leaveBeta("Threaded messages");
|
||||||
|
cy.wait(1000);
|
||||||
|
// after reload the property should be gone
|
||||||
|
cy.window().should("not.have.prop", "beforeReload");
|
||||||
|
});
|
||||||
|
|
||||||
|
cy.get(".mx_MatrixChat", { timeout: 15000 }); // wait for the app
|
||||||
|
markWindowBeforeReload();
|
||||||
|
|
||||||
|
// Turn on
|
||||||
|
cy.openUserSettings("Labs").within(() => {
|
||||||
|
// initially the new property is there
|
||||||
|
cy.window().should("have.prop", "beforeReload", true);
|
||||||
|
|
||||||
|
cy.joinBeta("Threaded messages");
|
||||||
|
cy.wait(1000);
|
||||||
|
// after reload the property should be gone
|
||||||
|
cy.window().should("not.have.prop", "beforeReload");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("should be usable for a conversation", () => {
|
it("should be usable for a conversation", () => {
|
||||||
let bot: MatrixClient;
|
let bot: MatrixClient;
|
||||||
cy.getBot(synapse, {
|
cy.getBot(synapse, {
|
||||||
|
@ -145,7 +145,7 @@ describe("Widget PIP", () => {
|
|||||||
win.mxActiveWidgetStore.setWidgetPersistence(DEMO_WIDGET_ID, roomId, true);
|
win.mxActiveWidgetStore.setWidgetPersistence(DEMO_WIDGET_ID, roomId, true);
|
||||||
|
|
||||||
// checks that pip window is opened
|
// checks that pip window is opened
|
||||||
cy.get(".mx_LegacyCallView_pip").should("exist");
|
cy.get(".mx_WidgetPip").should("exist");
|
||||||
|
|
||||||
// checks that widget is opened in pip
|
// checks that widget is opened in pip
|
||||||
cy.accessIframe(`iframe[title="${DEMO_WIDGET_NAME}"]`).within({}, () => {
|
cy.accessIframe(`iframe[title="${DEMO_WIDGET_NAME}"]`).within({}, () => {
|
||||||
@ -164,7 +164,7 @@ describe("Widget PIP", () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// checks that pip window is closed
|
// checks that pip window is closed
|
||||||
cy.get(".mx_LegacyCallView_pip").should("not.exist");
|
cy.get(".mx_WidgetPip").should("not.exist");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -71,8 +71,8 @@ Cypress.Commands.add("goOnline", (): void => {
|
|||||||
Cypress.Commands.add("stubDefaultServer", (): void => {
|
Cypress.Commands.add("stubDefaultServer", (): void => {
|
||||||
cy.log("Stubbing vector.im and matrix.org network calls");
|
cy.log("Stubbing vector.im and matrix.org network calls");
|
||||||
// We intercept vector.im & matrix.org calls so that tests don't fail when it has issues
|
// We intercept vector.im & matrix.org calls so that tests don't fail when it has issues
|
||||||
cy.intercept("GET", "https://vector.im/_matrix/identity/api/v1", {
|
cy.intercept("GET", "https://vector.im/_matrix/identity/v2", {
|
||||||
fixture: "vector-im-identity-v1.json",
|
fixture: "vector-im-identity-v2.json",
|
||||||
});
|
});
|
||||||
cy.intercept("GET", "https://matrix.org/.well-known/matrix/client", {
|
cy.intercept("GET", "https://matrix.org/.well-known/matrix/client", {
|
||||||
fixture: "matrix-org-client-well-known.json",
|
fixture: "matrix-org-client-well-known.json",
|
||||||
|
@ -1,29 +1,37 @@
|
|||||||
# Icons
|
# Icons
|
||||||
|
|
||||||
Icons are loaded using [@svgr/webpack](https://www.npmjs.com/package/@svgr/webpack). This is configured in [element-web](https://github.com/vector-im/element-web/blob/develop/webpack.config.js#L458)
|
Icons are loaded using [@svgr/webpack](https://www.npmjs.com/package/@svgr/webpack).
|
||||||
|
This is configured in [element-web](https://github.com/vector-im/element-web/blob/develop/webpack.config.js#L458).
|
||||||
|
|
||||||
Each .svg exports a `ReactComponent` at the named export `Icon`.
|
Each `.svg` exports a `ReactComponent` at the named export `Icon`.
|
||||||
Icons have `role="presentation"` and `aria-hidden` automatically applied. These can be overriden by passing props to the icon component.
|
Icons have `role="presentation"` and `aria-hidden` automatically applied. These can be overriden by passing props to the icon component.
|
||||||
|
|
||||||
eg
|
SVG file recommendations:
|
||||||
|
|
||||||
|
- Colours should not be defined absolutely. Use `currentColor` instead.
|
||||||
|
- There should not be a padding in SVG files. It should be added by CSS.
|
||||||
|
|
||||||
|
Example usage:
|
||||||
|
|
||||||
```
|
```
|
||||||
import { Icon as FavoriteIcon } from 'res/img/element-icons/favorite.svg';
|
import { Icon as FavoriteIcon } from 'res/img/element-icons/favorite.svg';
|
||||||
|
|
||||||
const MyComponent = () => {
|
const MyComponent = () => {
|
||||||
return <>
|
return <>
|
||||||
<FavoriteIcon>
|
<FavoriteIcon className="mx_Icon mx_Icon_16">
|
||||||
<FavoriteIcon className="mx_MyComponent-icon" role="img" aria-hidden="false">
|
|
||||||
</>;
|
</>;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Styling
|
If possible, use the icon classes from [here](../res/css/compound/_Icon.pcss).
|
||||||
|
|
||||||
Icon components are svg elements and can be styled as usual.
|
## Custom styling
|
||||||
|
|
||||||
```
|
Icon components are svg elements and may be custom styled as usual.
|
||||||
// _MyComponents.pcss
|
|
||||||
|
`_MyComponents.pcss`:
|
||||||
|
|
||||||
|
```css
|
||||||
.mx_MyComponent-icon {
|
.mx_MyComponent-icon {
|
||||||
height: 20px;
|
height: 20px;
|
||||||
width: 20px;
|
width: 20px;
|
||||||
@ -32,13 +40,15 @@ Icon components are svg elements and can be styled as usual.
|
|||||||
fill: $accent;
|
fill: $accent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
```
|
||||||
|
|
||||||
// MyComponent.tsx
|
`MyComponent.tsx`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
import { Icon as FavoriteIcon } from 'res/img/element-icons/favorite.svg';
|
import { Icon as FavoriteIcon } from 'res/img/element-icons/favorite.svg';
|
||||||
|
|
||||||
const MyComponent = () => {
|
const MyComponent = () => {
|
||||||
return <>
|
return <>
|
||||||
<FavoriteIcon>
|
|
||||||
<FavoriteIcon className="mx_MyComponent-icon" role="img" aria-hidden="false">
|
<FavoriteIcon className="mx_MyComponent-icon" role="img" aria-hidden="false">
|
||||||
</>;
|
</>;
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "matrix-react-sdk",
|
"name": "matrix-react-sdk",
|
||||||
"version": "3.62.0",
|
"version": "3.63.0",
|
||||||
"description": "SDK for matrix.org using React",
|
"description": "SDK for matrix.org using React",
|
||||||
"author": "matrix.org",
|
"author": "matrix.org",
|
||||||
"repository": {
|
"repository": {
|
||||||
@ -57,7 +57,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.12.5",
|
"@babel/runtime": "^7.12.5",
|
||||||
"@matrix-org/analytics-events": "^0.3.0",
|
"@matrix-org/analytics-events": "^0.3.0",
|
||||||
"@matrix-org/matrix-wysiwyg": "^0.9.0",
|
"@matrix-org/matrix-wysiwyg": "^0.13.0",
|
||||||
"@matrix-org/react-sdk-module-api": "^0.0.3",
|
"@matrix-org/react-sdk-module-api": "^0.0.3",
|
||||||
"@sentry/browser": "^7.0.0",
|
"@sentry/browser": "^7.0.0",
|
||||||
"@sentry/tracing": "^7.0.0",
|
"@sentry/tracing": "^7.0.0",
|
||||||
@ -134,7 +134,7 @@
|
|||||||
"@babel/register": "^7.12.10",
|
"@babel/register": "^7.12.10",
|
||||||
"@babel/traverse": "^7.12.12",
|
"@babel/traverse": "^7.12.12",
|
||||||
"@casualbot/jest-sonar-reporter": "^2.2.5",
|
"@casualbot/jest-sonar-reporter": "^2.2.5",
|
||||||
"@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.8.tgz",
|
"@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.14.tgz",
|
||||||
"@peculiar/webcrypto": "^1.4.1",
|
"@peculiar/webcrypto": "^1.4.1",
|
||||||
"@percy/cli": "^1.11.0",
|
"@percy/cli": "^1.11.0",
|
||||||
"@percy/cypress": "^3.1.2",
|
"@percy/cypress": "^3.1.2",
|
||||||
|
@ -30,6 +30,8 @@
|
|||||||
@import "./components/views/location/_ZoomButtons.pcss";
|
@import "./components/views/location/_ZoomButtons.pcss";
|
||||||
@import "./components/views/messages/_MBeaconBody.pcss";
|
@import "./components/views/messages/_MBeaconBody.pcss";
|
||||||
@import "./components/views/messages/shared/_MediaProcessingError.pcss";
|
@import "./components/views/messages/shared/_MediaProcessingError.pcss";
|
||||||
|
@import "./components/views/pips/_WidgetPip.pcss";
|
||||||
|
@import "./components/views/settings/devices/_CurrentDeviceSection.pcss";
|
||||||
@import "./components/views/settings/devices/_DeviceDetailHeading.pcss";
|
@import "./components/views/settings/devices/_DeviceDetailHeading.pcss";
|
||||||
@import "./components/views/settings/devices/_DeviceDetails.pcss";
|
@import "./components/views/settings/devices/_DeviceDetails.pcss";
|
||||||
@import "./components/views/settings/devices/_DeviceExpandDetailsButton.pcss";
|
@import "./components/views/settings/devices/_DeviceExpandDetailsButton.pcss";
|
||||||
@ -62,7 +64,6 @@
|
|||||||
@import "./structures/_MainSplit.pcss";
|
@import "./structures/_MainSplit.pcss";
|
||||||
@import "./structures/_MatrixChat.pcss";
|
@import "./structures/_MatrixChat.pcss";
|
||||||
@import "./structures/_NonUrgentToastContainer.pcss";
|
@import "./structures/_NonUrgentToastContainer.pcss";
|
||||||
@import "./structures/_NotificationPanel.pcss";
|
|
||||||
@import "./structures/_QuickSettingsButton.pcss";
|
@import "./structures/_QuickSettingsButton.pcss";
|
||||||
@import "./structures/_RightPanel.pcss";
|
@import "./structures/_RightPanel.pcss";
|
||||||
@import "./structures/_RoomSearch.pcss";
|
@import "./structures/_RoomSearch.pcss";
|
||||||
@ -308,6 +309,7 @@
|
|||||||
@import "./views/rooms/wysiwyg_composer/_SendWysiwygComposer.pcss";
|
@import "./views/rooms/wysiwyg_composer/_SendWysiwygComposer.pcss";
|
||||||
@import "./views/rooms/wysiwyg_composer/components/_Editor.pcss";
|
@import "./views/rooms/wysiwyg_composer/components/_Editor.pcss";
|
||||||
@import "./views/rooms/wysiwyg_composer/components/_FormattingButtons.pcss";
|
@import "./views/rooms/wysiwyg_composer/components/_FormattingButtons.pcss";
|
||||||
|
@import "./views/rooms/wysiwyg_composer/components/_LinkModal.pcss";
|
||||||
@import "./views/settings/_AvatarSetting.pcss";
|
@import "./views/settings/_AvatarSetting.pcss";
|
||||||
@import "./views/settings/_CrossSigningPanel.pcss";
|
@import "./views/settings/_CrossSigningPanel.pcss";
|
||||||
@import "./views/settings/_CryptographyPanel.pcss";
|
@import "./views/settings/_CryptographyPanel.pcss";
|
||||||
@ -372,7 +374,6 @@
|
|||||||
@import "./views/voip/_LegacyCallViewForRoom.pcss";
|
@import "./views/voip/_LegacyCallViewForRoom.pcss";
|
||||||
@import "./views/voip/_LegacyCallViewHeader.pcss";
|
@import "./views/voip/_LegacyCallViewHeader.pcss";
|
||||||
@import "./views/voip/_LegacyCallViewSidebar.pcss";
|
@import "./views/voip/_LegacyCallViewSidebar.pcss";
|
||||||
@import "./views/voip/_PiPContainer.pcss";
|
|
||||||
@import "./views/voip/_VideoFeed.pcss";
|
@import "./views/voip/_VideoFeed.pcss";
|
||||||
@import "./voice-broadcast/atoms/_LiveBadge.pcss";
|
@import "./voice-broadcast/atoms/_LiveBadge.pcss";
|
||||||
@import "./voice-broadcast/atoms/_PlaybackControlButton.pcss";
|
@import "./voice-broadcast/atoms/_PlaybackControlButton.pcss";
|
||||||
|
68
res/css/components/views/pips/_WidgetPip.pcss
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.mx_WidgetPip {
|
||||||
|
width: 320px;
|
||||||
|
height: 220px;
|
||||||
|
border-radius: 8px;
|
||||||
|
contain: paint;
|
||||||
|
color: $call-primary-content;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_WidgetPip_header,
|
||||||
|
.mx_WidgetPip_footer {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
height: 60px;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
transition: opacity ease 0.15s;
|
||||||
|
|
||||||
|
.mx_WidgetPip:not(:hover) > & {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_WidgetPip_header {
|
||||||
|
top: 0;
|
||||||
|
padding: $spacing-12;
|
||||||
|
display: flex;
|
||||||
|
font-size: $font-12px;
|
||||||
|
font-weight: $font-semi-bold;
|
||||||
|
background: linear-gradient(rgba(0, 0, 0, 0.9), rgba(0, 0, 0, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_WidgetPip_backButton {
|
||||||
|
height: $spacing-24;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: $spacing-12;
|
||||||
|
|
||||||
|
> .mx_Icon {
|
||||||
|
color: $call-light-quaternary-content;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_WidgetPip_footer {
|
||||||
|
bottom: 0;
|
||||||
|
padding: $spacing-12 $spacing-8;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: flex-end;
|
||||||
|
background: linear-gradient(rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.9));
|
||||||
|
}
|
@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export function htmlToPlainText(html: string) {
|
.mx_CurrentDeviceSection_deviceDetails {
|
||||||
return new DOMParser().parseFromString(html, "text/html").documentElement.textContent;
|
// align with text of session tile
|
||||||
|
margin-left: 56px;
|
||||||
}
|
}
|
@ -19,8 +19,6 @@ limitations under the License.
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
margin-top: $spacing-16;
|
margin-top: $spacing-16;
|
||||||
padding: $spacing-24;
|
padding: $spacing-24;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
@ -50,3 +50,8 @@ limitations under the License.
|
|||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
gap: $spacing-8;
|
gap: $spacing-8;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_FilteredDeviceList_deviceDetails {
|
||||||
|
// align with text of session tile
|
||||||
|
margin-left: 88px;
|
||||||
|
}
|
||||||
|
@ -1,113 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
.mx_NotificationPanel {
|
|
||||||
order: 2;
|
|
||||||
flex: 1 1 0;
|
|
||||||
overflow-y: auto;
|
|
||||||
display: flex;
|
|
||||||
|
|
||||||
.mx_RoomView_messageListWrapper {
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_RoomView_MessageList {
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
margin-left: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* FIXME: rather than having EventTile's default CSS be for MessagePanel,
|
|
||||||
we should make EventTile a base CSS class and customise it specifically
|
|
||||||
for usage in {Message,File,Notification}Panel. */
|
|
||||||
|
|
||||||
.mx_EventTile_avatar {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_EventTile {
|
|
||||||
word-break: break-word;
|
|
||||||
position: relative;
|
|
||||||
padding-block: 18px;
|
|
||||||
|
|
||||||
.mx_EventTile_senderDetails,
|
|
||||||
.mx_EventTile_line {
|
|
||||||
padding-left: 36px; /* align with the room name */
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_EventTile_senderDetails {
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
a {
|
|
||||||
display: flex;
|
|
||||||
column-gap: 5px; /* TODO: Use a spacing variable */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_DisambiguatedProfile,
|
|
||||||
.mx_MessageTimestamp {
|
|
||||||
color: $primary-content;
|
|
||||||
font-size: $font-12px;
|
|
||||||
display: inline;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover .mx_EventTile_line {
|
|
||||||
background-color: $background;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:not(.mx_EventTile_last):not(.mx_EventTile_lastInSection)::after {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
background-color: $tertiary-content;
|
|
||||||
height: 1px;
|
|
||||||
opacity: 0.4;
|
|
||||||
content: "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_EventTile_roomName {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
column-gap: $spacing-8;
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: $font-14px;
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: $primary-content;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_EventTile_selected .mx_EventTile_line {
|
|
||||||
padding-left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_EventTile_content {
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_NotificationPanel_empty::before {
|
|
||||||
mask-image: url("$(res)/img/element-icons/notifications.svg");
|
|
||||||
}
|
|
@ -38,9 +38,9 @@ $SpaceRoomViewInnerWidth: 428px;
|
|||||||
&::before {
|
&::before {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
content: "";
|
content: "";
|
||||||
width: 32px;
|
width: 24px;
|
||||||
height: 32px;
|
height: 24px;
|
||||||
top: 24px;
|
top: 27px;
|
||||||
left: 20px;
|
left: 20px;
|
||||||
mask-position: center;
|
mask-position: center;
|
||||||
mask-repeat: no-repeat;
|
mask-repeat: no-repeat;
|
||||||
|
@ -6,6 +6,10 @@
|
|||||||
mask-image: url("$(res)/img/element-icons/roomlist/low-priority.svg");
|
mask-image: url("$(res)/img/element-icons/roomlist/low-priority.svg");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_RoomGeneralContextMenu_iconMarkAsRead::before {
|
||||||
|
mask-image: url("$(res)/img/element-icons/roomlist/mark-as-read.svg");
|
||||||
|
}
|
||||||
|
|
||||||
.mx_RoomGeneralContextMenu_iconNotificationsDefault::before {
|
.mx_RoomGeneralContextMenu_iconNotificationsDefault::before {
|
||||||
mask-image: url("$(res)/img/element-icons/notifications.svg");
|
mask-image: url("$(res)/img/element-icons/notifications.svg");
|
||||||
}
|
}
|
||||||
|
@ -50,8 +50,10 @@ limitations under the License.
|
|||||||
}
|
}
|
||||||
|
|
||||||
.mx_JoinRuleDropdown_invite::before {
|
.mx_JoinRuleDropdown_invite::before {
|
||||||
|
box-sizing: border-box;
|
||||||
mask-image: url("$(res)/img/element-icons/lock.svg");
|
mask-image: url("$(res)/img/element-icons/lock.svg");
|
||||||
mask-size: contain;
|
mask-size: contain;
|
||||||
|
padding: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_JoinRuleDropdown_public::before {
|
.mx_JoinRuleDropdown_public::before {
|
||||||
|
@ -30,7 +30,7 @@ limitations under the License.
|
|||||||
}
|
}
|
||||||
|
|
||||||
.mx_BaseAvatar_image {
|
.mx_BaseAvatar_image {
|
||||||
border: 1px solid $background;
|
border: 1px solid var(--facepile-background, $background);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_BaseAvatar_initial {
|
.mx_BaseAvatar_initial {
|
||||||
|
@ -29,49 +29,70 @@ limitations under the License.
|
|||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: $spacing-8;
|
gap: $spacing-8;
|
||||||
|
|
||||||
.mx_CallEvent_title {
|
> .mx_BaseAvatar,
|
||||||
font-size: $font-15px;
|
> .mx_Icon {
|
||||||
line-height: 24px; /* in px to match the avatar */
|
|
||||||
}
|
|
||||||
|
|
||||||
&.mx_CallEvent_inactive .mx_CallEvent_title::before {
|
|
||||||
display: inline-block;
|
|
||||||
vertical-align: middle;
|
|
||||||
content: "";
|
|
||||||
background-color: $secondary-content;
|
|
||||||
mask-image: url("$(res)/img/element-icons/call/video-call.svg");
|
|
||||||
mask-size: 16px;
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
margin-right: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.mx_CallEvent_active .mx_CallEvent_title {
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .mx_BaseAvatar {
|
|
||||||
align-self: flex-start;
|
align-self: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
> .mx_CallEvent_infoRows {
|
> .mx_Icon {
|
||||||
flex-grow: 1;
|
padding: 0;
|
||||||
|
margin: $spacing-4 0;
|
||||||
display: flex;
|
color: $secondary-content;
|
||||||
flex-direction: column;
|
|
||||||
gap: $spacing-4;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
> .mx_CallDuration {
|
.mx_LiveContentSummary {
|
||||||
padding: $spacing-4;
|
font-size: $font-12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
> .mx_CallEvent_button {
|
--facepile-background: $system;
|
||||||
box-sizing: border-box;
|
}
|
||||||
min-width: 120px;
|
|
||||||
}
|
.mx_CallEvent_title {
|
||||||
|
font-size: $font-15px;
|
||||||
|
line-height: 24px; /* in px to match the avatar */
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_CallEvent_inactive .mx_CallEvent_title::before {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
content: "";
|
||||||
|
background-color: $secondary-content;
|
||||||
|
mask-image: url("$(res)/img/element-icons/call/video-call.svg");
|
||||||
|
mask-size: 16px;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
margin-right: $spacing-8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_CallEvent_active .mx_CallEvent_title {
|
||||||
|
font-weight: $font-semi-bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_CallEvent_columns {
|
||||||
|
flex-grow: 1;
|
||||||
|
display: flex;
|
||||||
|
gap: $spacing-12;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_TimelineCard .mx_CallEvent_columns {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: $spacing-8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_CallEvent_details {
|
||||||
|
flex-grow: 1;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_CallEvent_button {
|
||||||
|
box-sizing: border-box;
|
||||||
|
min-width: 120px;
|
||||||
}
|
}
|
||||||
|
@ -68,7 +68,7 @@ limitations under the License.
|
|||||||
&.mx_LegacyCallEvent_rejected,
|
&.mx_LegacyCallEvent_rejected,
|
||||||
&.mx_LegacyCallEvent_noAnswer {
|
&.mx_LegacyCallEvent_noAnswer {
|
||||||
.mx_LegacyCallEvent_type_icon::before {
|
.mx_LegacyCallEvent_type_icon::before {
|
||||||
mask-image: url("$(res)/img/voip/declined-voice.svg");
|
mask-image: url("$(res)/img/element-icons/call/hangup.svg");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -210,7 +210,6 @@ limitations under the License.
|
|||||||
|
|
||||||
.mx_FilePanel,
|
.mx_FilePanel,
|
||||||
.mx_UserInfo,
|
.mx_UserInfo,
|
||||||
.mx_NotificationPanel,
|
|
||||||
.mx_MemberList {
|
.mx_MemberList {
|
||||||
&.mx_BaseCard {
|
&.mx_BaseCard {
|
||||||
padding: $spacing-32 0 0;
|
padding: $spacing-32 0 0;
|
||||||
|
@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
$MiniAppTileHeight: 200px;
|
$MiniAppTileHeight: 220px;
|
||||||
/* TODO this should be 300px but that's too large */
|
/* TODO this should be 300px but that's too large */
|
||||||
$MinWidth: 240px;
|
$MinWidth: 240px;
|
||||||
|
|
||||||
|
@ -836,7 +836,8 @@ $left-gutter: 64px;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_EventTile[data-shape="ThreadsList"] {
|
.mx_EventTile[data-shape="ThreadsList"],
|
||||||
|
.mx_EventTile[data-shape="Notification"] {
|
||||||
--topOffset: $spacing-12;
|
--topOffset: $spacing-12;
|
||||||
--leftOffset: 48px;
|
--leftOffset: 48px;
|
||||||
$borderRadius: 8px;
|
$borderRadius: 8px;
|
||||||
@ -916,9 +917,7 @@ $left-gutter: 64px;
|
|||||||
}
|
}
|
||||||
|
|
||||||
.mx_DisambiguatedProfile {
|
.mx_DisambiguatedProfile {
|
||||||
margin-inline: 0 $spacing-12;
|
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
flex: 1;
|
|
||||||
|
|
||||||
.mx_DisambiguatedProfile_displayName,
|
.mx_DisambiguatedProfile_displayName,
|
||||||
.mx_DisambiguatedProfile_mxid {
|
.mx_DisambiguatedProfile_mxid {
|
||||||
@ -941,6 +940,7 @@ $left-gutter: 64px;
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
padding-bottom: 0;
|
padding-bottom: 0;
|
||||||
|
padding-inline-start: var(--leftOffset);
|
||||||
|
|
||||||
.mx_ThreadPanel_replies {
|
.mx_ThreadPanel_replies {
|
||||||
margin-top: $spacing-8;
|
margin-top: $spacing-8;
|
||||||
@ -966,11 +966,6 @@ $left-gutter: 64px;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_DisambiguatedProfile,
|
|
||||||
.mx_EventTile_line {
|
|
||||||
padding-inline-start: var(--leftOffset);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_MessageTimestamp {
|
.mx_MessageTimestamp {
|
||||||
font-size: $font-12px;
|
font-size: $font-12px;
|
||||||
max-width: var(--MessageTimestamp-max-width);
|
max-width: var(--MessageTimestamp-max-width);
|
||||||
@ -1300,6 +1295,21 @@ $left-gutter: 64px;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_EventTile_details {
|
||||||
|
display: flex;
|
||||||
|
width: -webkit-fill-available;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: $spacing-8;
|
||||||
|
margin-left: var(--leftOffset);
|
||||||
|
.mx_EventTile_truncated {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Media query for mobile UI */
|
/* Media query for mobile UI */
|
||||||
@media only screen and (max-width: 480px) {
|
@media only screen and (max-width: 480px) {
|
||||||
.mx_EventTile_content {
|
.mx_EventTile_content {
|
||||||
|
@ -254,7 +254,7 @@ limitations under the License.
|
|||||||
}
|
}
|
||||||
|
|
||||||
.mx_MessageComposer_voiceMessage::before {
|
.mx_MessageComposer_voiceMessage::before {
|
||||||
mask-image: url("$(res)/img/voip/mic-on-mask.svg");
|
mask-image: url("$(res)/img/element-icons/mic.svg");
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_MessageComposer_voiceBroadcast::before {
|
.mx_MessageComposer_voiceBroadcast::before {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
@ -14,15 +14,16 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.mx_PiPContainer {
|
.mx_LinkModal {
|
||||||
position: absolute;
|
padding: $spacing-32;
|
||||||
right: 20px;
|
|
||||||
bottom: 72px;
|
|
||||||
z-index: 100;
|
|
||||||
|
|
||||||
/* Disable pointer events for Jitsi widgets to function. Direct */
|
.mx_Dialog_content {
|
||||||
/* calls have their own cursor and behaviour, but we need to make */
|
margin-top: 30px;
|
||||||
/* sure the cursor hits the iframe for Jitsi which will be at a */
|
margin-bottom: 42px;
|
||||||
/* different level. */
|
}
|
||||||
pointer-events: none;
|
|
||||||
|
.mx_LinkModal_content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
}
|
}
|
@ -58,8 +58,10 @@ $spacePanelWidth: 68px;
|
|||||||
.mx_SpaceCreateMenuType_public::before {
|
.mx_SpaceCreateMenuType_public::before {
|
||||||
mask-image: url("$(res)/img/globe.svg");
|
mask-image: url("$(res)/img/globe.svg");
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_SpaceCreateMenuType_private::before {
|
.mx_SpaceCreateMenuType_private::before {
|
||||||
mask-image: url("$(res)/img/element-icons/lock.svg");
|
mask-image: url("$(res)/img/element-icons/lock.svg");
|
||||||
|
mask-size: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_SpaceCreateMenu_back {
|
.mx_SpaceCreateMenu_back {
|
||||||
|
@ -94,7 +94,9 @@ limitations under the License.
|
|||||||
}
|
}
|
||||||
|
|
||||||
&.mx_LegacyCallViewButtons_button_mic::before {
|
&.mx_LegacyCallViewButtons_button_mic::before {
|
||||||
mask-image: url("$(res)/img/voip/call-view/mic-on.svg");
|
height: 20px;
|
||||||
|
mask-image: url("$(res)/img/element-icons/mic.svg");
|
||||||
|
width: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.mx_LegacyCallViewButtons_button_vid::before {
|
&.mx_LegacyCallViewButtons_button_vid::before {
|
||||||
@ -123,7 +125,9 @@ limitations under the License.
|
|||||||
}
|
}
|
||||||
|
|
||||||
&.mx_LegacyCallViewButtons_button_mic::before {
|
&.mx_LegacyCallViewButtons_button_mic::before {
|
||||||
mask-image: url("$(res)/img/voip/call-view/mic-off.svg");
|
height: 20px;
|
||||||
|
mask-image: url("$(res)/img/element-icons/Mic-off.svg");
|
||||||
|
width: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.mx_LegacyCallViewButtons_button_vid::before {
|
&.mx_LegacyCallViewButtons_button_vid::before {
|
||||||
@ -159,7 +163,7 @@ limitations under the License.
|
|||||||
background-color: $alert;
|
background-color: $alert;
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
mask-image: url("$(res)/img/voip/call-view/hangup.svg");
|
mask-image: url("$(res)/img/element-icons/call/hangup.svg");
|
||||||
background-color: white; /* Same on both themes */
|
background-color: white; /* Same on both themes */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -32,7 +32,7 @@ limitations under the License.
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: inherit;
|
border-radius: inherit;
|
||||||
background-color: $call-lobby-background;
|
background-color: $call-background;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* While the lobby is shown, the widget needs to stay loaded but hidden in the background */
|
/* While the lobby is shown, the widget needs to stay loaded but hidden in the background */
|
||||||
@ -44,8 +44,10 @@ limitations under the License.
|
|||||||
min-height: 0;
|
min-height: 0;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
padding: $spacing-12;
|
padding: $spacing-12;
|
||||||
color: $call-lobby-primary-content;
|
color: $call-primary-content;
|
||||||
background-color: $call-lobby-background;
|
background-color: $call-background;
|
||||||
|
|
||||||
|
--facepile-background: $call-background;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -57,10 +59,6 @@ limitations under the License.
|
|||||||
.mx_FacePile {
|
.mx_FacePile {
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
margin: $spacing-8 auto 0;
|
margin: $spacing-8 auto 0;
|
||||||
|
|
||||||
.mx_FacePile_faces .mx_BaseAvatar_image {
|
|
||||||
border-color: $call-lobby-background;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_CallView_preview {
|
.mx_CallView_preview {
|
||||||
@ -68,7 +66,7 @@ limitations under the License.
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 800px;
|
max-width: 800px;
|
||||||
aspect-ratio: 1.5;
|
aspect-ratio: 1.5;
|
||||||
background-color: $call-lobby-system;
|
background-color: $call-system;
|
||||||
|
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@ -106,7 +104,7 @@ limitations under the License.
|
|||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
|
|
||||||
background-color: rgba($call-lobby-background, 0.9);
|
background-color: rgba($call-background, 0.9);
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@ -122,7 +120,7 @@ limitations under the License.
|
|||||||
width: $size;
|
width: $size;
|
||||||
height: $size;
|
height: $size;
|
||||||
|
|
||||||
background-color: $call-lobby-system;
|
background-color: $call-system;
|
||||||
border-radius: calc($size / 2);
|
border-radius: calc($size / 2);
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
@ -131,13 +129,14 @@ limitations under the License.
|
|||||||
mask-repeat: no-repeat;
|
mask-repeat: no-repeat;
|
||||||
mask-size: 20px;
|
mask-size: 20px;
|
||||||
mask-position: center;
|
mask-position: center;
|
||||||
background-color: $call-lobby-primary-content;
|
background-color: $call-primary-content;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.mx_CallView_deviceButton_audio::before {
|
&.mx_CallView_deviceButton_audio::before {
|
||||||
mask-image: url("$(res)/img/voip/call-view/mic-on.svg");
|
mask-image: url("$(res)/img/element-icons/mic.svg");
|
||||||
|
mask-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.mx_CallView_deviceButton_video::before {
|
&.mx_CallView_deviceButton_video::before {
|
||||||
@ -154,7 +153,7 @@ limitations under the License.
|
|||||||
width: $size;
|
width: $size;
|
||||||
height: $size;
|
height: $size;
|
||||||
|
|
||||||
background-color: $call-lobby-system;
|
background-color: $call-system;
|
||||||
border-radius: calc($size / 2);
|
border-radius: calc($size / 2);
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
@ -163,7 +162,7 @@ limitations under the License.
|
|||||||
mask-image: url("$(res)/img/feather-customised/chevron-down.svg");
|
mask-image: url("$(res)/img/feather-customised/chevron-down.svg");
|
||||||
mask-size: $size;
|
mask-size: $size;
|
||||||
mask-position: center;
|
mask-position: center;
|
||||||
background-color: $call-lobby-primary-content;
|
background-color: $call-primary-content;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
@ -172,16 +171,17 @@ limitations under the License.
|
|||||||
&.mx_CallView_deviceButtonWrapper_muted {
|
&.mx_CallView_deviceButtonWrapper_muted {
|
||||||
.mx_CallView_deviceButton,
|
.mx_CallView_deviceButton,
|
||||||
.mx_CallView_deviceListButton {
|
.mx_CallView_deviceListButton {
|
||||||
background-color: $call-lobby-primary-content;
|
background-color: $call-primary-content;
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
background-color: $call-lobby-system;
|
background-color: $call-system;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_CallView_deviceButton {
|
.mx_CallView_deviceButton {
|
||||||
&.mx_CallView_deviceButton_audio::before {
|
&.mx_CallView_deviceButton_audio::before {
|
||||||
mask-image: url("$(res)/img/voip/call-view/mic-off.svg");
|
mask-image: url("$(res)/img/element-icons/Mic-off.svg");
|
||||||
|
mask-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.mx_CallView_deviceButton_video::before {
|
&.mx_CallView_deviceButton_video::before {
|
||||||
|
@ -15,11 +15,15 @@ limitations under the License.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
.mx_LegacyCallPreview {
|
.mx_LegacyCallPreview {
|
||||||
position: fixed;
|
align-items: flex-end;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: $spacing-16;
|
||||||
left: 0;
|
left: 0;
|
||||||
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
/* Display above any widget elements */
|
||||||
pointer-events: initial; /* restore pointer events so the user can leave/interact */
|
z-index: 102;
|
||||||
|
|
||||||
.mx_VideoFeed_remote.mx_VideoFeed_voice {
|
.mx_VideoFeed_remote.mx_VideoFeed_voice {
|
||||||
min-height: 150px;
|
min-height: 150px;
|
||||||
|
@ -70,8 +70,8 @@ limitations under the License.
|
|||||||
&::before {
|
&::before {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
content: "";
|
content: "";
|
||||||
width: 16px;
|
width: 17px;
|
||||||
height: 16px;
|
height: 17px;
|
||||||
mask-repeat: no-repeat;
|
mask-repeat: no-repeat;
|
||||||
mask-size: contain;
|
mask-size: contain;
|
||||||
mask-position: center;
|
mask-position: center;
|
||||||
@ -80,11 +80,11 @@ limitations under the License.
|
|||||||
}
|
}
|
||||||
|
|
||||||
&.mx_VideoFeed_mic_muted::before {
|
&.mx_VideoFeed_mic_muted::before {
|
||||||
mask-image: url("$(res)/img/voip/mic-muted.svg");
|
mask-image: url("$(res)/img/element-icons/Mic-off.svg");
|
||||||
}
|
}
|
||||||
|
|
||||||
&.mx_VideoFeed_mic_unmuted::before {
|
&.mx_VideoFeed_mic_unmuted::before {
|
||||||
mask-image: url("$(res)/img/voip/mic-unmuted.svg");
|
mask-image: url("$(res)/img/element-icons/mic.svg");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
5
res/img/element-icons/Mic-off.svg
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<svg width="26" height="26" viewBox="0 0 26 26" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M2.21924 0.380761C1.71155 -0.12692 0.888441 -0.12692 0.380761 0.380761C-0.12692 0.888441 -0.12692 1.71155 0.380761 2.21924L7.79998 9.63846V13C7.79998 15.8719 10.1281 18.2 13 18.2C13.9643 18.2 14.8674 17.9375 15.6416 17.48L17.5195 19.358C16.2459 20.2649 14.6882 20.7988 13.0059 20.8L13 20.8L12.9941 20.8C8.68896 20.7968 5.19999 17.3058 5.19999 13C5.19999 12.282 4.61796 11.7 3.89999 11.7C3.18202 11.7 2.59999 12.282 2.59999 13C2.59999 18.3035 6.56977 22.6798 11.7 23.3195V24.7C11.7 25.4179 12.282 26 13 26C13.7179 26 14.3 25.4179 14.3 24.7V23.3195C16.1989 23.0827 17.9388 22.334 19.3773 21.2158L23.7808 25.6192C24.2884 26.1269 25.1115 26.1269 25.6192 25.6192C26.1269 25.1115 26.1269 24.2884 25.6192 23.7808L2.21924 0.380761Z" fill="currentColor"/>
|
||||||
|
<path d="M20.5292 15.0447L22.5665 17.0862C23.103 15.8318 23.4 14.4506 23.4 13C23.4 12.282 22.818 11.7 22.1 11.7C21.382 11.7 20.8 12.282 20.8 13C20.8 13.7075 20.7058 14.393 20.5292 15.0447Z" fill="currentColor"/>
|
||||||
|
<path d="M8.36 2.85008L18.2 12.7106V5.20001C18.2 2.32813 15.8719 1.87345e-05 13 1.87345e-05C10.9737 1.87345e-05 9.21819 1.15894 8.36 2.85008Z" fill="currentColor"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.2 KiB |
@ -1,3 +1,4 @@
|
|||||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path d="M12.0084 7.75648C10.3211 7.69163 6.85136 8.12949 6.00781 8.35133C5.95792 8.36445 5.90044 8.37912 5.83616 8.39552C4.54101 8.72607 0.48272 9.76183 0.0442423 13.0436C-0.295466 15.5862 1.40575 16.3558 2.25618 16.2386C2.84479 16.1648 4.5301 15.8983 6.08724 15.6189C7.61629 15.3446 7.61551 14.3359 7.61499 13.6538C7.61498 13.6413 7.61497 13.6288 7.61497 13.6165L7.61497 12.2453C7.61497 11.8961 7.94315 11.6942 8.3958 11.6396C9.99822 11.422 11.3359 11.4213 12.0055 11.4213L12.0112 11.4213C12.6807 11.4213 14.0018 11.422 15.6042 11.6396C16.0569 11.6942 16.385 11.8961 16.385 12.2453L16.385 13.6165C16.385 13.6289 16.385 13.6413 16.385 13.6538C16.3845 14.3359 16.3837 15.3446 17.9128 15.619C19.4699 15.8983 21.1552 16.1648 21.7438 16.2386C22.5942 16.3558 24.2955 15.5862 23.9558 13.0436C23.5173 9.76183 19.459 8.72608 18.1638 8.39553C18.0996 8.37913 18.0421 8.36446 17.9922 8.35134C17.1487 8.1295 13.6956 7.69163 12.0084 7.75648Z" fill="black"/>
|
<path d="M8.02698 15.9613C9.16801 17.1932 11.9148 19.3263 12.6635 19.7641C12.7078 19.79 12.7585 19.8201 12.8152 19.8538C13.9576 20.5329 17.5373 22.6609 20.1454 20.6694C22.1661 19.1266 21.5091 17.3909 20.8289 16.875C20.3633 16.5128 18.9914 15.5145 17.7006 14.6152C16.4331 13.7322 15.7268 14.4397 15.2492 14.918C15.2404 14.9268 15.2317 14.9355 15.2231 14.9442L14.2621 15.9051C14.0174 16.1498 13.6451 16.0605 13.2886 15.7804C12.0092 14.8061 11.0681 13.8659 10.5972 13.395L10.5933 13.391C10.1225 12.9202 9.19387 11.9908 8.21957 10.7114C7.93949 10.3548 7.85018 9.9826 8.09489 9.73789L9.05586 8.77693C9.06448 8.7683 9.0732 8.7596 9.08199 8.75082C9.56034 8.27321 10.2678 7.56684 9.38479 6.29937C8.48555 5.0086 7.4872 3.6367 7.125 3.17106C6.60907 2.49094 4.87345 1.83392 3.33056 3.85458C1.33907 6.46274 3.46708 10.0424 4.1462 11.1848C4.17991 11.2415 4.21005 11.2922 4.23593 11.3365C4.67367 12.0851 6.79507 14.8202 8.02698 15.9613Z" fill="currentColor"/>
|
||||||
|
<path d="M20.6971 3.07817C20.94 2.83153 20.94 2.43163 20.6971 2.18499C20.4542 1.93834 20.0603 1.93834 19.8174 2.18499L17 5.04555L14.1826 2.18499C13.9397 1.93834 13.5458 1.93834 13.3029 2.18499C13.06 2.43163 13.06 2.83153 13.3029 3.07817L16.1203 5.93873L13.1822 8.92183C12.9393 9.16847 12.9393 9.56837 13.1822 9.81501C13.4251 10.0617 13.819 10.0617 14.0619 9.81501L17 6.83192L19.9381 9.81501C20.181 10.0617 20.5749 10.0617 20.8178 9.81501C21.0607 9.56837 21.0607 9.16847 20.8178 8.92182L17.8797 5.93873L20.6971 3.07817Z" fill="currentColor"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.6 KiB |
@ -1,4 +1,4 @@
|
|||||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path d="M0 7C0 5.34315 1.34315 4 3 4H14C15.6569 4 17 5.34315 17 7V17C17 18.6569 15.6569 20 14 20H3C1.34315 20 0 18.6569 0 17V7Z" fill="white"/>
|
<path d="M0 7C0 5.34315 1.34315 4 3 4H14C15.6569 4 17 5.34315 17 7V17C17 18.6569 15.6569 20 14 20H3C1.34315 20 0 18.6569 0 17V7Z" fill="currentColor"/>
|
||||||
<path d="M19 9L22.3753 6.29976C23.0301 5.77595 24 6.24212 24 7.08062V16.9194C24 17.7579 23.0301 18.2241 22.3753 17.7002L19 15V9Z" fill="white"/>
|
<path d="M19 9L22.3753 6.29976C23.0301 5.77595 24 6.24212 24 7.08062V16.9194C24 17.7579 23.0301 18.2241 22.3753 17.7002L19 15V9Z" fill="currentColor"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 393 B After Width: | Height: | Size: 407 B |
Before Width: | Height: | Size: 450 B After Width: | Height: | Size: 450 B |
3
res/img/element-icons/room/composer/link.svg
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M8.37751 4.10063L8.73382 3.74433C9.718 2.76019 11.3042 2.75074 12.2767 3.72324C13.2493 4.69573 13.2398 6.2819 12.2556 7.26605L10.5627 8.9589C9.57856 9.94304 7.99234 9.95249 7.01981 8.98M7.62266 11.8992L7.26619 12.2557C6.28201 13.2398 4.69578 13.2493 3.72325 12.2768C2.75073 11.3043 2.76018 9.7181 3.74436 8.73395L5.43717 7.0412C6.42134 6.05706 8.00756 6.04761 8.98009 7.0201" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 554 B |
3
res/img/element-icons/roomlist/mark-as-read.svg
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M3.34474 4.9093C3.03141 5.10514 3.03141 5.56147 3.34474 5.7573L7.73468 8.50101C7.89681 8.60234 8.10254 8.60234 8.26467 8.50101L12.6546 5.7573C12.9679 5.56147 12.9679 5.10514 12.6546 4.9093L8.26467 2.16559C8.10254 2.06426 7.89681 2.06426 7.73468 2.16559L3.34474 4.9093ZM14.6663 5.3333V12C14.6663 12.3536 14.5259 12.6927 14.2758 12.9428C14.0258 13.1928 13.6866 13.3333 13.333 13.3333H2.66634C2.31272 13.3333 1.97358 13.1928 1.72353 12.9428C1.47348 12.6927 1.33301 12.3536 1.33301 12V5.3333C1.33301 4.84664 1.59301 4.42664 1.97967 4.1933L7.99967 0.426636L14.0197 4.1933C14.4063 4.42664 14.6663 4.84664 14.6663 5.3333Z" fill="currentColor"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 750 B |
@ -1,3 +0,0 @@
|
|||||||
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M11.0071 7.39308C9.57295 7.33796 6.62364 7.71014 5.90662 7.8987C5.86422 7.90986 5.81537 7.92232 5.76073 7.93627C4.65985 8.21723 1.2103 9.09763 0.837594 11.8872C0.548842 14.0484 1.99488 14.7025 2.71774 14.6029C3.21806 14.5402 4.65057 14.3136 5.97414 14.0762C7.27383 13.843 7.27317 12.9856 7.27273 12.4058C7.27272 12.3951 7.27271 12.3846 7.27271 12.3741L7.27271 11.2085C7.27271 10.9117 7.55166 10.7401 7.93642 10.6937C9.29847 10.5087 10.4355 10.5082 11.0047 10.5082L11.0095 10.5082C11.5786 10.5082 12.7015 10.5087 14.0636 10.6938C14.4483 10.7401 14.7273 10.9117 14.7273 11.2085L14.7273 12.3741C14.7273 12.3846 14.7273 12.3952 14.7273 12.4058C14.7268 12.9856 14.7261 13.843 16.0258 14.0762C17.3494 14.3136 18.7819 14.5402 19.2822 14.6029C20.0051 14.7025 21.4511 14.0484 21.1624 11.8872C20.7897 9.09763 17.3401 8.21724 16.2393 7.93628C16.1846 7.92233 16.1358 7.90986 16.0934 7.89871C15.3763 7.71015 12.4412 7.33796 11.0071 7.39308Z" fill="white"/>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 1.0 KiB |
@ -1,3 +0,0 @@
|
|||||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.70711 2.29289C3.31658 1.90237 2.68342 1.90237 2.29289 2.29289C1.90237 2.68342 1.90237 3.31658 2.29289 3.70711L8 9.41421V12C8 14.2091 9.79086 16 12 16C12.7418 16 13.4365 15.7981 14.032 15.4462L15.4765 16.8907C14.4957 17.5892 13.2958 18 12 18C8.68629 18 6 15.3137 6 12C6 11.4477 5.55228 11 5 11C4.44772 11 4 11.4477 4 12C4 16.0796 7.05369 19.446 11 19.9381V21C11 21.5523 11.4477 22 12 22C12.5523 22 13 21.5523 13 21V19.9381C14.4607 19.756 15.7991 19.18 16.9056 18.3199L20.2929 21.7071C20.6834 22.0976 21.3166 22.0976 21.7071 21.7071C22.0976 21.3166 22.0976 20.6834 21.7071 20.2929L3.70711 2.29289ZM17.7917 13.5729L19.3589 15.1433C19.7715 14.1783 20 13.1159 20 12C20 11.4477 19.5523 11 19 11C18.4477 11 18 11.4477 18 12C18 12.5443 17.9275 13.0716 17.7917 13.5729ZM8.43077 4.19238L16 11.7774V6C16 3.79086 14.2091 2 12 2C10.4413 2 9.09091 2.89149 8.43077 4.19238Z" fill="#21262C"/>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 1.0 KiB |
@ -1,3 +0,0 @@
|
|||||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.09999 6.15C8.09999 3.99609 9.84608 2.25 12 2.25C14.1539 2.25 15.9 3.99609 15.9 6.15V11.9825C15.9 14.1365 14.1539 15.8825 12 15.8825C9.84608 15.8825 8.09999 14.1365 8.09999 11.9825V6.15ZM5.1748 10.7375C5.86516 10.7375 6.4248 11.2972 6.4248 11.9875C6.4248 15.0574 8.91483 17.5493 11.9915 17.5538C11.9943 17.5538 11.9972 17.5538 12 17.5538C12.0028 17.5538 12.0056 17.5538 12.0084 17.5538C15.085 17.5492 17.5748 15.0573 17.5748 11.9875C17.5748 11.2972 18.1344 10.7375 18.8248 10.7375C19.5152 10.7375 20.0748 11.2972 20.0748 11.9875C20.0748 16.0189 17.115 19.3576 13.25 19.9577V20.7513C13.25 21.4416 12.6904 22.0013 12 22.0013C11.3096 22.0013 10.75 21.4416 10.75 20.7513V19.9578C6.88482 19.3578 3.9248 16.0191 3.9248 11.9875C3.9248 11.2972 4.48445 10.7375 5.1748 10.7375Z" fill="currentColor"/>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 945 B |
@ -1,4 +0,0 @@
|
|||||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M5.35116 10.6409C6.11185 11.4622 7.94306 12.8843 8.44217 13.1761C8.47169 13.1934 8.50549 13.2135 8.54328 13.2359C9.30489 13.6887 11.6913 15.1074 13.4301 13.7797C14.7772 12.7511 14.3392 11.594 13.8858 11.2501C13.5754 11.0086 12.6608 10.3431 11.8003 9.74356C10.9553 9.15489 10.4844 9.62653 10.1659 9.94543C10.1601 9.95129 10.1543 9.9571 10.1485 9.96285L9.50791 10.6035C9.34477 10.7666 9.0966 10.7071 8.8589 10.5204C8.00599 9.87084 7.37856 9.24399 7.06465 8.93008L7.06201 8.92744C6.74815 8.61357 6.12909 7.99392 5.47955 7.14101C5.29283 6.90331 5.23329 6.65515 5.39643 6.49201L6.03708 5.85136C6.04283 5.84561 6.04864 5.83981 6.0545 5.83396C6.3734 5.51555 6.84504 5.04464 6.25636 4.19966C5.65687 3.33915 4.9913 2.42455 4.74984 2.11412C4.40588 1.66071 3.2488 1.22269 2.22021 2.5698C0.89255 4.30858 2.31122 6.69502 2.76397 7.45663C2.78644 7.49443 2.80653 7.52822 2.82379 7.55774C3.11562 8.05685 4.52989 9.88025 5.35116 10.6409Z" fill="#737D8C"/>
|
|
||||||
<path d="M13.7979 2.05203C13.9599 1.8876 13.9599 1.62101 13.7979 1.45658C13.636 1.29214 13.3734 1.29214 13.2114 1.45658L11.3332 3.36362L9.4549 1.45658C9.29295 1.29214 9.03037 1.29214 8.86842 1.45658C8.70647 1.62101 8.70647 1.8876 8.86842 2.05203L10.7467 3.95907L8.78797 5.9478C8.62602 6.11223 8.62602 6.37883 8.78797 6.54326C8.94992 6.70769 9.21249 6.70769 9.37444 6.54326L11.3332 4.55453L13.2919 6.54326C13.4538 6.70769 13.7164 6.70769 13.8784 6.54326C14.0403 6.37883 14.0403 6.11223 13.8784 5.9478L11.9196 3.95907L13.7979 2.05203Z" fill="#737D8C"/>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 1.6 KiB |
@ -1,5 +0,0 @@
|
|||||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M1.9206 1.0544C1.68141 0.815201 1.29359 0.815201 1.0544 1.0544C0.815201 1.29359 0.815201 1.68141 1.0544 1.9206L4.55 5.41621V7C4.55 8.3531 5.6469 9.45 7 9.45C7.45436 9.45 7.87983 9.32632 8.24458 9.11079L9.12938 9.99558C8.52863 10.4234 7.7937 10.675 7 10.675C4.97035 10.675 3.325 9.02965 3.325 7C3.325 6.66173 3.05077 6.3875 2.7125 6.3875C2.37423 6.3875 2.1 6.66173 2.1 7C2.1 9.49877 3.97038 11.5607 6.3875 11.8621V12.5125C6.3875 12.8508 6.66173 13.125 7 13.125C7.33827 13.125 7.6125 12.8508 7.6125 12.5125V11.8621C8.50718 11.7505 9.32696 11.3978 10.0047 10.8709L12.0794 12.9456C12.3186 13.1848 12.7064 13.1848 12.9456 12.9456C13.1848 12.7064 13.1848 12.3186 12.9456 12.0794L1.9206 1.0544Z" fill="white"/>
|
|
||||||
<path d="M10.5474 7.96338L11.5073 8.92525C11.7601 8.33424 11.9 7.68346 11.9 7C11.9 6.66173 11.6258 6.3875 11.2875 6.3875C10.9492 6.3875 10.675 6.66173 10.675 7C10.675 7.33336 10.6306 7.65634 10.5474 7.96338Z" fill="white"/>
|
|
||||||
<path d="M4.81385 2.21784L9.45 6.86366V3.325C9.45 1.9719 8.3531 0.875 7 0.875C6.04532 0.875 5.21818 1.42104 4.81385 2.21784Z" fill="white"/>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 1.2 KiB |
@ -1,3 +0,0 @@
|
|||||||
<svg width="20" height="21" viewBox="0 0 20 21" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.09999 4.15C6.09999 1.99609 7.84608 0.25 9.99999 0.25C12.1539 0.25 13.9 1.99609 13.9 4.15V9.98254C13.9 12.1365 12.1539 13.8825 9.99999 13.8825C7.84608 13.8825 6.09999 12.1365 6.09999 9.98254V4.15ZM3.1748 8.73755C3.86516 8.73755 4.4248 9.29719 4.4248 9.98755C4.4248 13.0574 6.91483 15.5493 9.9915 15.5538C9.99433 15.5538 9.99717 15.5538 10 15.5538C10.0028 15.5538 10.0056 15.5538 10.0084 15.5538C13.085 15.5492 15.5748 13.0573 15.5748 9.98755C15.5748 9.29719 16.1344 8.73755 16.8248 8.73755C17.5152 8.73755 18.0748 9.29719 18.0748 9.98755C18.0748 14.0189 15.115 17.3576 11.25 17.9577V18.7513C11.25 19.4416 10.6904 20.0013 10 20.0013C9.30964 20.0013 8.75 19.4416 8.75 18.7513V17.9578C4.88483 17.3578 1.9248 14.0191 1.9248 9.98755C1.9248 9.29719 2.48445 8.73755 3.1748 8.73755Z" fill="#C1C6CD"/>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 951 B |
@ -1,4 +0,0 @@
|
|||||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M4.4645 3.29384C4.4645 1.95795 5.59973 0.875 7.0001 0.875C8.40048 0.875 9.53571 1.95795 9.53571 3.29384V6.91127C9.53571 8.24716 8.40048 9.33011 7.0001 9.33011C5.59973 9.33011 4.4645 8.24716 4.4645 6.91127V3.29384Z" fill="white"/>
|
|
||||||
<path d="M2.56269 6.1391C3.01153 6.1391 3.37539 6.4862 3.37539 6.91437C3.37539 8.81701 4.99198 10.3617 6.99032 10.3666C6.99359 10.3666 6.99686 10.3666 7.00014 10.3666C7.0034 10.3666 7.00665 10.3666 7.0099 10.3666C9.00814 10.3616 10.6246 8.81694 10.6246 6.91437C10.6246 6.4862 10.9885 6.1391 11.4373 6.1391C11.8861 6.1391 12.25 6.4862 12.25 6.91437C12.25 9.41469 10.3257 11.4854 7.81283 11.8576V12.3497C7.81283 12.7779 7.44898 13.125 7.00014 13.125C6.5513 13.125 6.18744 12.7779 6.18744 12.3497V11.8576C3.67448 11.4855 1.75 9.41478 1.75 6.91437C1.75 6.4862 2.11386 6.1391 2.56269 6.1391Z" fill="white"/>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 945 B |
@ -188,9 +188,10 @@ $call-view-content-background: $quinary-content;
|
|||||||
|
|
||||||
$video-feed-secondary-background: $system;
|
$video-feed-secondary-background: $system;
|
||||||
|
|
||||||
$call-lobby-system: $system;
|
$call-system: $system;
|
||||||
$call-lobby-background: $background;
|
$call-background: $background;
|
||||||
$call-lobby-primary-content: $primary-content;
|
$call-primary-content: $primary-content;
|
||||||
|
$call-light-quaternary-content: #c1c6cd;
|
||||||
/* ******************** */
|
/* ******************** */
|
||||||
|
|
||||||
/* Location sharing */
|
/* Location sharing */
|
||||||
|
@ -119,9 +119,10 @@ $call-view-content-background: $quinary-content;
|
|||||||
|
|
||||||
$video-feed-secondary-background: $system;
|
$video-feed-secondary-background: $system;
|
||||||
|
|
||||||
$call-lobby-system: $system;
|
$call-system: $system;
|
||||||
$call-lobby-background: $background;
|
$call-background: $background;
|
||||||
$call-lobby-primary-content: $primary-content;
|
$call-primary-content: $primary-content;
|
||||||
|
$call-light-quaternary-content: #c1c6cd;
|
||||||
|
|
||||||
$roomlist-filter-active-bg-color: $panel-actions;
|
$roomlist-filter-active-bg-color: $panel-actions;
|
||||||
$roomlist-bg-color: $header-panel-bg-color;
|
$roomlist-bg-color: $header-panel-bg-color;
|
||||||
|
@ -4,17 +4,20 @@
|
|||||||
Arial empirically gets it right, hence prioritising Arial here.
|
Arial empirically gets it right, hence prioritising Arial here.
|
||||||
We also include STIXGeneral explicitly to support a wider range
|
We also include STIXGeneral explicitly to support a wider range
|
||||||
of combining diacritics (Chrome fails without it, as per
|
of combining diacritics (Chrome fails without it, as per
|
||||||
https://bugs.chromium.org/p/chromium/issues/detail?id=1328898) */
|
https://bugs.chromium.org/p/chromium/issues/detail?id=1328898).
|
||||||
|
We should never actively *prefer* STIXGeneral over the default font though,
|
||||||
|
since it looks pretty rough and implements some non-LGC scripts only
|
||||||
|
partially, making, for example, Japanese text look patchy and sad. */
|
||||||
/* We fall through to Twemoji for emoji rather than falling through
|
/* We fall through to Twemoji for emoji rather than falling through
|
||||||
to native Emoji fonts (if any) to ensure cross-browser consistency */
|
to native Emoji fonts (if any) to ensure cross-browser consistency */
|
||||||
/* Noto Color Emoji contains digits, in fixed-width, therefore causing
|
/* Noto Color Emoji contains digits, in fixed-width, therefore causing
|
||||||
digits in flowed text to stand out.
|
digits in flowed text to stand out.
|
||||||
TODO: Consider putting all emoji fonts to the end rather than the front. */
|
TODO: Consider putting all emoji fonts to the end rather than the front. */
|
||||||
$font-family: "Nunito", "Twemoji", "Apple Color Emoji", "Segoe UI Emoji", "STIXGeneral", "Arial", "Helvetica",
|
$font-family: "Nunito", "Twemoji", "Apple Color Emoji", "Segoe UI Emoji", "Arial", "Helvetica", sans-serif,
|
||||||
sans-serif, "Noto Color Emoji";
|
"STIXGeneral", "Noto Color Emoji";
|
||||||
|
|
||||||
$monospace-font-family: "Inconsolata", "Twemoji", "Apple Color Emoji", "Segoe UI Emoji", "STIXGeneral", "Courier",
|
$monospace-font-family: "Inconsolata", "Twemoji", "Apple Color Emoji", "Segoe UI Emoji", "Courier", monospace,
|
||||||
monospace, "Noto Color Emoji";
|
"STIXGeneral", "Noto Color Emoji";
|
||||||
|
|
||||||
/* unified palette */
|
/* unified palette */
|
||||||
/* try to use these colors when possible */
|
/* try to use these colors when possible */
|
||||||
@ -182,9 +185,10 @@ $call-view-content-background: #21262c;
|
|||||||
$video-feed-secondary-background: #394049; /* XXX: Color from dark theme */
|
$video-feed-secondary-background: #394049; /* XXX: Color from dark theme */
|
||||||
|
|
||||||
/* All of these are from dark theme */
|
/* All of these are from dark theme */
|
||||||
$call-lobby-system: #21262c;
|
$call-system: #21262c;
|
||||||
$call-lobby-background: #15191e;
|
$call-background: #15191e;
|
||||||
$call-lobby-primary-content: #ffffff;
|
$call-primary-content: #ffffff;
|
||||||
|
$call-light-quaternary-content: #c1c6cd;
|
||||||
|
|
||||||
$username-variant1-color: #368bd6;
|
$username-variant1-color: #368bd6;
|
||||||
$username-variant2-color: #ac3ba8;
|
$username-variant2-color: #ac3ba8;
|
||||||
|
@ -4,17 +4,20 @@
|
|||||||
Arial empirically gets it right, hence prioritising Arial here.
|
Arial empirically gets it right, hence prioritising Arial here.
|
||||||
We also include STIXGeneral explicitly to support a wider range
|
We also include STIXGeneral explicitly to support a wider range
|
||||||
of combining diacritics (Chrome fails without it, as per
|
of combining diacritics (Chrome fails without it, as per
|
||||||
https://bugs.chromium.org/p/chromium/issues/detail?id=1328898) */
|
https://bugs.chromium.org/p/chromium/issues/detail?id=1328898).
|
||||||
|
We should never actively *prefer* STIXGeneral over the default font though,
|
||||||
|
since it looks pretty rough and implements some non-LGC scripts only
|
||||||
|
partially, making, for example, Japanese text look patchy and sad. */
|
||||||
/* We fall through to Twemoji for emoji rather than falling through
|
/* We fall through to Twemoji for emoji rather than falling through
|
||||||
to native Emoji fonts (if any) to ensure cross-browser consistency */
|
to native Emoji fonts (if any) to ensure cross-browser consistency */
|
||||||
/* Noto Color Emoji contains digits, in fixed-width, therefore causing
|
/* Noto Color Emoji contains digits, in fixed-width, therefore causing
|
||||||
digits in flowed text to stand out.
|
digits in flowed text to stand out.
|
||||||
TODO: Consider putting all emoji fonts to the end rather than the front. */
|
TODO: Consider putting all emoji fonts to the end rather than the front. */
|
||||||
$font-family: "Inter", "Twemoji", "Apple Color Emoji", "Segoe UI Emoji", "STIXGeneral", "Arial", "Helvetica", sans-serif,
|
$font-family: "Inter", "Twemoji", "Apple Color Emoji", "Segoe UI Emoji", "Arial", "Helvetica", sans-serif, "STIXGeneral",
|
||||||
"Noto Color Emoji";
|
"Noto Color Emoji";
|
||||||
|
|
||||||
$monospace-font-family: "Inconsolata", "Twemoji", "Apple Color Emoji", "Segoe UI Emoji", "STIXGeneral", "Courier",
|
$monospace-font-family: "Inconsolata", "Twemoji", "Apple Color Emoji", "Segoe UI Emoji", "Courier", monospace,
|
||||||
monospace, "Noto Color Emoji";
|
"STIXGeneral", "Noto Color Emoji";
|
||||||
|
|
||||||
/* Colors from Figma Compound https://www.figma.com/file/X4XTH9iS2KGJ2wFKDqkyed/Compound?node-id=559%3A120 */
|
/* Colors from Figma Compound https://www.figma.com/file/X4XTH9iS2KGJ2wFKDqkyed/Compound?node-id=559%3A120 */
|
||||||
/* ******************** */
|
/* ******************** */
|
||||||
@ -273,9 +276,11 @@ $video-feed-secondary-background: #394049; /* XXX: Color from dark theme */
|
|||||||
$voipcall-plinth-color: $system;
|
$voipcall-plinth-color: $system;
|
||||||
|
|
||||||
/* All of these are from dark theme */
|
/* All of these are from dark theme */
|
||||||
$call-lobby-system: #21262c;
|
$call-system: #21262c;
|
||||||
$call-lobby-background: #15191e;
|
$call-background: #15191e;
|
||||||
$call-lobby-primary-content: #ffffff;
|
$call-primary-content: #ffffff;
|
||||||
|
/* This one is from light theme */
|
||||||
|
$call-light-quaternary-content: #c1c6cd;
|
||||||
/* ******************** */
|
/* ******************** */
|
||||||
|
|
||||||
/* One-off colors */
|
/* One-off colors */
|
||||||
|
@ -303,6 +303,7 @@ export async function uploadFile(
|
|||||||
progressHandler,
|
progressHandler,
|
||||||
abortController,
|
abortController,
|
||||||
includeFilename: false,
|
includeFilename: false,
|
||||||
|
type: "application/octet-stream",
|
||||||
});
|
});
|
||||||
if (abortController.signal.aborted) throw new UploadCanceledError();
|
if (abortController.signal.aborted) throw new UploadCanceledError();
|
||||||
|
|
||||||
|
@ -50,6 +50,7 @@ import { localNotificationsAreSilenced, createLocalNotificationSettingsIfNeeded
|
|||||||
import { getIncomingCallToastKey, IncomingCallToast } from "./toasts/IncomingCallToast";
|
import { getIncomingCallToastKey, IncomingCallToast } from "./toasts/IncomingCallToast";
|
||||||
import ToastStore from "./stores/ToastStore";
|
import ToastStore from "./stores/ToastStore";
|
||||||
import { ElementCall } from "./models/Call";
|
import { ElementCall } from "./models/Call";
|
||||||
|
import { VoiceBroadcastChunkEventType } from "./voice-broadcast";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Dispatches:
|
* Dispatches:
|
||||||
@ -77,6 +78,13 @@ const msgTypeHandlers = {
|
|||||||
[M_LOCATION.altName]: (event: MatrixEvent) => {
|
[M_LOCATION.altName]: (event: MatrixEvent) => {
|
||||||
return TextForEvent.textForLocationEvent(event)();
|
return TextForEvent.textForLocationEvent(event)();
|
||||||
},
|
},
|
||||||
|
[MsgType.Audio]: (event: MatrixEvent): string | null => {
|
||||||
|
if (event.getContent()?.[VoiceBroadcastChunkEventType]) {
|
||||||
|
// mute broadcast chunks
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return TextForEvent.textForEvent(event);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Notifier = {
|
export const Notifier = {
|
||||||
|
@ -134,7 +134,7 @@ export class PosthogAnalytics {
|
|||||||
private readonly enabled: boolean = false;
|
private readonly enabled: boolean = false;
|
||||||
private static _instance = null;
|
private static _instance = null;
|
||||||
private platformSuperProperties = {};
|
private platformSuperProperties = {};
|
||||||
private static ANALYTICS_EVENT_TYPE = "im.vector.analytics";
|
public static readonly ANALYTICS_EVENT_TYPE = "im.vector.analytics";
|
||||||
private propertiesForNextEvent: Partial<Record<"$set" | "$set_once", UserProperties>> = {};
|
private propertiesForNextEvent: Partial<Record<"$set" | "$set_once", UserProperties>> = {};
|
||||||
private userPropertyCache: UserProperties = {};
|
private userPropertyCache: UserProperties = {};
|
||||||
private authenticationType: Signup["authenticationType"] = "Other";
|
private authenticationType: Signup["authenticationType"] = "Other";
|
||||||
|
27
src/Rooms.ts
@ -57,42 +57,47 @@ export function guessAndSetDMRoom(room: Room, isDirect: boolean): Promise<void>
|
|||||||
/**
|
/**
|
||||||
* Marks or unmarks the given room as being as a DM room.
|
* Marks or unmarks the given room as being as a DM room.
|
||||||
* @param {string} roomId The ID of the room to modify
|
* @param {string} roomId The ID of the room to modify
|
||||||
* @param {string} userId The user ID of the desired DM
|
* @param {string | null} userId The user ID of the desired DM room target user or
|
||||||
room target user or null to un-mark
|
* null to un-mark this room as a DM room
|
||||||
this room as a DM room
|
|
||||||
* @returns {object} A promise
|
* @returns {object} A promise
|
||||||
*/
|
*/
|
||||||
export async function setDMRoom(roomId: string, userId: string): Promise<void> {
|
export async function setDMRoom(roomId: string, userId: string | null): Promise<void> {
|
||||||
if (MatrixClientPeg.get().isGuest()) return;
|
if (MatrixClientPeg.get().isGuest()) return;
|
||||||
|
|
||||||
const mDirectEvent = MatrixClientPeg.get().getAccountData(EventType.Direct);
|
const mDirectEvent = MatrixClientPeg.get().getAccountData(EventType.Direct);
|
||||||
let dmRoomMap = {};
|
const currentContent = mDirectEvent?.getContent() || {};
|
||||||
|
|
||||||
if (mDirectEvent !== undefined) dmRoomMap = { ...mDirectEvent.getContent() }; // copy as we will mutate
|
const dmRoomMap = new Map(Object.entries(currentContent));
|
||||||
|
let modified = false;
|
||||||
|
|
||||||
// remove it from the lists of any others users
|
// remove it from the lists of any others users
|
||||||
// (it can only be a DM room for one person)
|
// (it can only be a DM room for one person)
|
||||||
for (const thisUserId of Object.keys(dmRoomMap)) {
|
for (const thisUserId of dmRoomMap.keys()) {
|
||||||
const roomList = dmRoomMap[thisUserId];
|
const roomList = dmRoomMap.get(thisUserId) || [];
|
||||||
|
|
||||||
if (thisUserId != userId) {
|
if (thisUserId != userId) {
|
||||||
const indexOfRoom = roomList.indexOf(roomId);
|
const indexOfRoom = roomList.indexOf(roomId);
|
||||||
if (indexOfRoom > -1) {
|
if (indexOfRoom > -1) {
|
||||||
roomList.splice(indexOfRoom, 1);
|
roomList.splice(indexOfRoom, 1);
|
||||||
|
modified = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// now add it, if it's not already there
|
// now add it, if it's not already there
|
||||||
if (userId) {
|
if (userId) {
|
||||||
const roomList = dmRoomMap[userId] || [];
|
const roomList = dmRoomMap.get(userId) || [];
|
||||||
if (roomList.indexOf(roomId) == -1) {
|
if (roomList.indexOf(roomId) == -1) {
|
||||||
roomList.push(roomId);
|
roomList.push(roomId);
|
||||||
|
modified = true;
|
||||||
}
|
}
|
||||||
dmRoomMap[userId] = roomList;
|
dmRoomMap.set(userId, roomList);
|
||||||
}
|
}
|
||||||
|
|
||||||
await MatrixClientPeg.get().setAccountData(EventType.Direct, dmRoomMap);
|
// prevent unnecessary calls to setAccountData
|
||||||
|
if (!modified) return;
|
||||||
|
|
||||||
|
await MatrixClientPeg.get().setAccountData(EventType.Direct, Object.fromEntries(dmRoomMap));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -264,10 +264,36 @@ Get an openID token for the current user session.
|
|||||||
Request: No parameters
|
Request: No parameters
|
||||||
Response:
|
Response:
|
||||||
- The openId token object as described in https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3useruseridopenidrequest_token
|
- The openId token object as described in https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3useruseridopenidrequest_token
|
||||||
|
|
||||||
|
send_event
|
||||||
|
----------
|
||||||
|
Sends an event in a room.
|
||||||
|
|
||||||
|
Request:
|
||||||
|
- type is the event type to send.
|
||||||
|
- state_key is the state key to send. Omitted if not a state event.
|
||||||
|
- content is the event content to send.
|
||||||
|
|
||||||
|
Response:
|
||||||
|
- room_id is the room ID where the event was sent.
|
||||||
|
- event_id is the event ID of the event which was sent.
|
||||||
|
|
||||||
|
read_events
|
||||||
|
-----------
|
||||||
|
Read events from a room.
|
||||||
|
|
||||||
|
Request:
|
||||||
|
- type is the event type to read.
|
||||||
|
- state_key is the state key to read, or `true` to read all events of the type. Omitted if not a state event.
|
||||||
|
|
||||||
|
Response:
|
||||||
|
- events: Array of events. If none found, this will be an empty array.
|
||||||
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
import { IContent, MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
import { IEvent } from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
import { MatrixClientPeg } from "./MatrixClientPeg";
|
import { MatrixClientPeg } from "./MatrixClientPeg";
|
||||||
import dis from "./dispatcher/dispatcher";
|
import dis from "./dispatcher/dispatcher";
|
||||||
@ -295,6 +321,8 @@ enum Action {
|
|||||||
SetBotOptions = "set_bot_options",
|
SetBotOptions = "set_bot_options",
|
||||||
SetBotPower = "set_bot_power",
|
SetBotPower = "set_bot_power",
|
||||||
GetOpenIdToken = "get_open_id_token",
|
GetOpenIdToken = "get_open_id_token",
|
||||||
|
SendEvent = "send_event",
|
||||||
|
ReadEvents = "read_events",
|
||||||
}
|
}
|
||||||
|
|
||||||
function sendResponse(event: MessageEvent<any>, res: any): void {
|
function sendResponse(event: MessageEvent<any>, res: any): void {
|
||||||
@ -468,13 +496,13 @@ function setWidget(event: MessageEvent<any>, roomId: string | null): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getWidgets(event: MessageEvent<any>, roomId: string): void {
|
function getWidgets(event: MessageEvent<any>, roomId: string | null): void {
|
||||||
const client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
if (!client) {
|
if (!client) {
|
||||||
sendError(event, _t("You need to be logged in."));
|
sendError(event, _t("You need to be logged in."));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let widgetStateEvents = [];
|
let widgetStateEvents: Partial<IEvent>[] = [];
|
||||||
|
|
||||||
if (roomId) {
|
if (roomId) {
|
||||||
const room = client.getRoom(roomId);
|
const room = client.getRoom(roomId);
|
||||||
@ -693,6 +721,141 @@ async function getOpenIdToken(event: MessageEvent<any>) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function sendEvent(
|
||||||
|
event: MessageEvent<{
|
||||||
|
type: string;
|
||||||
|
state_key?: string;
|
||||||
|
content?: IContent;
|
||||||
|
}>,
|
||||||
|
roomId: string,
|
||||||
|
) {
|
||||||
|
const eventType = event.data.type;
|
||||||
|
const stateKey = event.data.state_key;
|
||||||
|
const content = event.data.content;
|
||||||
|
|
||||||
|
if (typeof eventType !== "string") {
|
||||||
|
sendError(event, _t("Failed to send event"), new Error("Invalid 'type' in request"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const allowedEventTypes = ["m.widgets", "im.vector.modular.widgets", "io.element.integrations.installations"];
|
||||||
|
if (!allowedEventTypes.includes(eventType)) {
|
||||||
|
sendError(event, _t("Failed to send event"), new Error("Disallowed 'type' in request"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!content || typeof content !== "object") {
|
||||||
|
sendError(event, _t("Failed to send event"), new Error("Invalid 'content' in request"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = MatrixClientPeg.get();
|
||||||
|
if (!client) {
|
||||||
|
sendError(event, _t("You need to be logged in."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const room = client.getRoom(roomId);
|
||||||
|
if (!room) {
|
||||||
|
sendError(event, _t("This room is not recognised."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stateKey !== undefined) {
|
||||||
|
// state event
|
||||||
|
try {
|
||||||
|
const res = await client.sendStateEvent(roomId, eventType, content, stateKey);
|
||||||
|
sendResponse(event, {
|
||||||
|
room_id: roomId,
|
||||||
|
event_id: res.event_id,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
sendError(event, _t("Failed to send event"), e as Error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// message event
|
||||||
|
sendError(event, _t("Failed to send event"), new Error("Sending message events is not implemented"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readEvents(
|
||||||
|
event: MessageEvent<{
|
||||||
|
type: string;
|
||||||
|
state_key?: string | boolean;
|
||||||
|
limit?: number;
|
||||||
|
}>,
|
||||||
|
roomId: string,
|
||||||
|
) {
|
||||||
|
const eventType = event.data.type;
|
||||||
|
const stateKey = event.data.state_key;
|
||||||
|
const limit = event.data.limit;
|
||||||
|
|
||||||
|
if (typeof eventType !== "string") {
|
||||||
|
sendError(event, _t("Failed to read events"), new Error("Invalid 'type' in request"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const allowedEventTypes = [
|
||||||
|
"m.room.power_levels",
|
||||||
|
"m.room.encryption",
|
||||||
|
"m.room.member",
|
||||||
|
"m.room.name",
|
||||||
|
"m.widgets",
|
||||||
|
"im.vector.modular.widgets",
|
||||||
|
"io.element.integrations.installations",
|
||||||
|
];
|
||||||
|
if (!allowedEventTypes.includes(eventType)) {
|
||||||
|
sendError(event, _t("Failed to read events"), new Error("Disallowed 'type' in request"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let effectiveLimit: number;
|
||||||
|
if (limit !== undefined) {
|
||||||
|
if (typeof limit !== "number" || limit < 0) {
|
||||||
|
sendError(event, _t("Failed to read events"), new Error("Invalid 'limit' in request"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
effectiveLimit = Math.min(limit, Number.MAX_SAFE_INTEGER);
|
||||||
|
} else {
|
||||||
|
effectiveLimit = Number.MAX_SAFE_INTEGER;
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = MatrixClientPeg.get();
|
||||||
|
if (!client) {
|
||||||
|
sendError(event, _t("You need to be logged in."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const room = client.getRoom(roomId);
|
||||||
|
if (!room) {
|
||||||
|
sendError(event, _t("This room is not recognised."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stateKey !== undefined) {
|
||||||
|
// state events
|
||||||
|
if (typeof stateKey !== "string" && stateKey !== true) {
|
||||||
|
sendError(event, _t("Failed to read events"), new Error("Invalid 'state_key' in request"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// When `true` is passed for state key, get events with any state key.
|
||||||
|
const effectiveStateKey = stateKey === true ? undefined : stateKey;
|
||||||
|
|
||||||
|
let events: MatrixEvent[] = [];
|
||||||
|
events = events.concat(room.currentState.getStateEvents(eventType, effectiveStateKey as string) || []);
|
||||||
|
events = events.slice(0, effectiveLimit);
|
||||||
|
|
||||||
|
sendResponse(event, {
|
||||||
|
events: events.map((e) => e.getEffectiveEvent()),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
// message events
|
||||||
|
sendError(event, _t("Failed to read events"), new Error("Reading message events is not implemented"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const onMessage = function (event: MessageEvent<any>): void {
|
const onMessage = function (event: MessageEvent<any>): void {
|
||||||
if (!event.origin) {
|
if (!event.origin) {
|
||||||
// stupid chrome
|
// stupid chrome
|
||||||
@ -786,6 +949,12 @@ const onMessage = function (event: MessageEvent<any>): void {
|
|||||||
} else if (event.data.action === Action.CanSendEvent) {
|
} else if (event.data.action === Action.CanSendEvent) {
|
||||||
canSendEvent(event, roomId);
|
canSendEvent(event, roomId);
|
||||||
return;
|
return;
|
||||||
|
} else if (event.data.action === Action.SendEvent) {
|
||||||
|
sendEvent(event, roomId);
|
||||||
|
return;
|
||||||
|
} else if (event.data.action === Action.ReadEvents) {
|
||||||
|
readEvents(event, roomId);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
|
@ -41,7 +41,6 @@ import { DefaultTagID } from "../../stores/room-list/models";
|
|||||||
import { hideToast as hideServerLimitToast, showToast as showServerLimitToast } from "../../toasts/ServerLimitToast";
|
import { hideToast as hideServerLimitToast, showToast as showServerLimitToast } from "../../toasts/ServerLimitToast";
|
||||||
import { Action } from "../../dispatcher/actions";
|
import { Action } from "../../dispatcher/actions";
|
||||||
import LeftPanel from "./LeftPanel";
|
import LeftPanel from "./LeftPanel";
|
||||||
import PipContainer from "../views/voip/PipContainer";
|
|
||||||
import { ViewRoomDeltaPayload } from "../../dispatcher/payloads/ViewRoomDeltaPayload";
|
import { ViewRoomDeltaPayload } from "../../dispatcher/payloads/ViewRoomDeltaPayload";
|
||||||
import RoomListStore from "../../stores/room-list/RoomListStore";
|
import RoomListStore from "../../stores/room-list/RoomListStore";
|
||||||
import NonUrgentToastContainer from "./NonUrgentToastContainer";
|
import NonUrgentToastContainer from "./NonUrgentToastContainer";
|
||||||
@ -71,6 +70,7 @@ import { SwitchSpacePayload } from "../../dispatcher/payloads/SwitchSpacePayload
|
|||||||
import { IConfigOptions } from "../../IConfigOptions";
|
import { IConfigOptions } from "../../IConfigOptions";
|
||||||
import LeftPanelLiveShareWarning from "../views/beacon/LeftPanelLiveShareWarning";
|
import LeftPanelLiveShareWarning from "../views/beacon/LeftPanelLiveShareWarning";
|
||||||
import { UserOnboardingPage } from "../views/user-onboarding/UserOnboardingPage";
|
import { UserOnboardingPage } from "../views/user-onboarding/UserOnboardingPage";
|
||||||
|
import { PipContainer } from "./PipContainer";
|
||||||
|
|
||||||
// We need to fetch each pinned message individually (if we don't already have it)
|
// We need to fetch each pinned message individually (if we don't already have it)
|
||||||
// so each pinned message may trigger a request. Limit the number per room for sanity.
|
// so each pinned message may trigger a request. Limit the number per room for sanity.
|
||||||
|
@ -25,6 +25,7 @@ import Spinner from "../views/elements/Spinner";
|
|||||||
import { Layout } from "../../settings/enums/Layout";
|
import { Layout } from "../../settings/enums/Layout";
|
||||||
import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext";
|
import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext";
|
||||||
import Measured from "../views/elements/Measured";
|
import Measured from "../views/elements/Measured";
|
||||||
|
import Heading from "../views/typography/Heading";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
onClose(): void;
|
onClose(): void;
|
||||||
@ -90,8 +91,21 @@ export default class NotificationPanel extends React.PureComponent<IProps, IStat
|
|||||||
narrow: this.state.narrow,
|
narrow: this.state.narrow,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<BaseCard className="mx_NotificationPanel" onClose={this.props.onClose} withoutScrollContainer>
|
<BaseCard
|
||||||
<Measured sensor={this.card.current} onMeasurement={this.onMeasurement} />
|
header={
|
||||||
|
<Heading size="h4" className="mx_BaseCard_header_title_heading">
|
||||||
|
{_t("Notifications")}
|
||||||
|
</Heading>
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Need to rename this CSS class to something more generic
|
||||||
|
* Will be done once all the panels are using a similar layout
|
||||||
|
*/
|
||||||
|
className="mx_ThreadPanel"
|
||||||
|
onClose={this.props.onClose}
|
||||||
|
withoutScrollContainer={true}
|
||||||
|
>
|
||||||
|
{this.card.current && <Measured sensor={this.card.current} onMeasurement={this.onMeasurement} />}
|
||||||
{content}
|
{content}
|
||||||
</BaseCard>
|
</BaseCard>
|
||||||
</RoomContext.Provider>
|
</RoomContext.Provider>
|
||||||
|
@ -16,9 +16,9 @@ limitations under the License.
|
|||||||
|
|
||||||
import React, { createRef } from "react";
|
import React, { createRef } from "react";
|
||||||
|
|
||||||
import UIStore, { UI_EVENTS } from "../../../stores/UIStore";
|
import UIStore, { UI_EVENTS } from "../../stores/UIStore";
|
||||||
import { lerp } from "../../../utils/AnimationUtils";
|
import { lerp } from "../../utils/AnimationUtils";
|
||||||
import { MarkedExecution } from "../../../utils/MarkedExecution";
|
import { MarkedExecution } from "../../utils/MarkedExecution";
|
||||||
|
|
||||||
const PIP_VIEW_WIDTH = 336;
|
const PIP_VIEW_WIDTH = 336;
|
||||||
const PIP_VIEW_HEIGHT = 232;
|
const PIP_VIEW_HEIGHT = 232;
|
||||||
@ -47,7 +47,7 @@ interface IChildrenOptions {
|
|||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
children: CreatePipChildren;
|
children: Array<CreatePipChildren>;
|
||||||
draggable: boolean;
|
draggable: boolean;
|
||||||
onDoubleClick?: () => void;
|
onDoubleClick?: () => void;
|
||||||
onMove?: () => void;
|
onMove?: () => void;
|
||||||
@ -65,12 +65,20 @@ export default class PictureInPictureDragger extends React.Component<IProps> {
|
|||||||
private desiredTranslationY = UIStore.instance.windowHeight - PADDING.bottom - PIP_VIEW_HEIGHT;
|
private desiredTranslationY = UIStore.instance.windowHeight - PADDING.bottom - PIP_VIEW_HEIGHT;
|
||||||
private translationX = this.desiredTranslationX;
|
private translationX = this.desiredTranslationX;
|
||||||
private translationY = this.desiredTranslationY;
|
private translationY = this.desiredTranslationY;
|
||||||
private moving = false;
|
private mouseHeld = false;
|
||||||
private scheduledUpdate = new MarkedExecution(
|
private scheduledUpdate: MarkedExecution = new MarkedExecution(
|
||||||
() => this.animationCallback(),
|
() => this.animationCallback(),
|
||||||
() => requestAnimationFrame(() => this.scheduledUpdate.trigger()),
|
() => requestAnimationFrame(() => this.scheduledUpdate.trigger()),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
private _moving = false;
|
||||||
|
public get moving(): boolean {
|
||||||
|
return this._moving;
|
||||||
|
}
|
||||||
|
private set moving(value: boolean) {
|
||||||
|
this._moving = value;
|
||||||
|
}
|
||||||
|
|
||||||
public componentDidMount() {
|
public componentDidMount() {
|
||||||
document.addEventListener("mousemove", this.onMoving);
|
document.addEventListener("mousemove", this.onMoving);
|
||||||
document.addEventListener("mouseup", this.onEndMoving);
|
document.addEventListener("mouseup", this.onEndMoving);
|
||||||
@ -85,6 +93,10 @@ export default class PictureInPictureDragger extends React.Component<IProps> {
|
|||||||
UIStore.instance.off(UI_EVENTS.Resize, this.onResize);
|
UIStore.instance.off(UI_EVENTS.Resize, this.onResize);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public componentDidUpdate(prevProps: Readonly<IProps>): void {
|
||||||
|
if (prevProps.children !== this.props.children) this.snap(true);
|
||||||
|
}
|
||||||
|
|
||||||
private animationCallback = () => {
|
private animationCallback = () => {
|
||||||
if (
|
if (
|
||||||
!this.moving &&
|
!this.moving &&
|
||||||
@ -179,42 +191,68 @@ export default class PictureInPictureDragger extends React.Component<IProps> {
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
||||||
this.moving = true;
|
this.mouseHeld = true;
|
||||||
this.initX = event.pageX - this.desiredTranslationX;
|
|
||||||
this.initY = event.pageY - this.desiredTranslationY;
|
|
||||||
this.scheduledUpdate.mark();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
private onMoving = (event: React.MouseEvent | MouseEvent) => {
|
private onMoving = (event: MouseEvent) => {
|
||||||
if (!this.moving) return;
|
if (!this.mouseHeld) return;
|
||||||
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
||||||
|
if (!this.moving) {
|
||||||
|
this.moving = true;
|
||||||
|
this.initX = event.pageX - this.desiredTranslationX;
|
||||||
|
this.initY = event.pageY - this.desiredTranslationY;
|
||||||
|
this.scheduledUpdate.mark();
|
||||||
|
}
|
||||||
|
|
||||||
this.setTranslation(event.pageX - this.initX, event.pageY - this.initY);
|
this.setTranslation(event.pageX - this.initX, event.pageY - this.initY);
|
||||||
};
|
};
|
||||||
|
|
||||||
private onEndMoving = () => {
|
private onEndMoving = (event: MouseEvent) => {
|
||||||
this.moving = false;
|
if (!this.mouseHeld) return;
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
this.mouseHeld = false;
|
||||||
|
// Delaying this to the next event loop tick is necessary for click
|
||||||
|
// event cancellation to work
|
||||||
|
setImmediate(() => (this.moving = false));
|
||||||
this.snap(true);
|
this.snap(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private onClickCapture = (event: React.MouseEvent) => {
|
||||||
|
// To prevent mouse up events during dragging from being double-counted
|
||||||
|
// as clicks, we cancel clicks before they ever reach the target
|
||||||
|
if (this.moving) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
const style = {
|
const style = {
|
||||||
transform: `translateX(${this.translationX}px) translateY(${this.translationY}px)`,
|
transform: `translateX(${this.translationX}px) translateY(${this.translationY}px)`,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const children = this.props.children.map((create: CreatePipChildren) => {
|
||||||
|
return create({
|
||||||
|
onStartMoving: this.onStartMoving,
|
||||||
|
onResize: this.onResize,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside
|
<aside
|
||||||
className={this.props.className}
|
className={this.props.className}
|
||||||
style={style}
|
style={style}
|
||||||
ref={this.callViewWrapper}
|
ref={this.callViewWrapper}
|
||||||
|
onClickCapture={this.onClickCapture}
|
||||||
onDoubleClick={this.props.onDoubleClick}
|
onDoubleClick={this.props.onDoubleClick}
|
||||||
>
|
>
|
||||||
{this.props.children({
|
{children}
|
||||||
onStartMoving: this.onStartMoving,
|
|
||||||
onResize: this.onResize,
|
|
||||||
})}
|
|
||||||
</aside>
|
</aside>
|
||||||
);
|
);
|
||||||
}
|
}
|
@ -14,28 +14,22 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { createRef, useContext } from "react";
|
import React, { MutableRefObject, useContext, useRef } from "react";
|
||||||
import { CallEvent, CallState, MatrixCall } from "matrix-js-sdk/src/webrtc/call";
|
import { CallEvent, CallState, MatrixCall } from "matrix-js-sdk/src/webrtc/call";
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
import classNames from "classnames";
|
|
||||||
import { Room } from "matrix-js-sdk/src/models/room";
|
|
||||||
import { Optional } from "matrix-events-sdk";
|
import { Optional } from "matrix-events-sdk";
|
||||||
|
|
||||||
import LegacyCallView from "./LegacyCallView";
|
import LegacyCallView from "../views/voip/LegacyCallView";
|
||||||
import LegacyCallHandler, { LegacyCallHandlerEvent } from "../../../LegacyCallHandler";
|
import LegacyCallHandler, { LegacyCallHandlerEvent } from "../../LegacyCallHandler";
|
||||||
import PersistentApp from "../elements/PersistentApp";
|
import { MatrixClientPeg } from "../../MatrixClientPeg";
|
||||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
|
||||||
import PictureInPictureDragger, { CreatePipChildren } from "./PictureInPictureDragger";
|
import PictureInPictureDragger, { CreatePipChildren } from "./PictureInPictureDragger";
|
||||||
import dis from "../../../dispatcher/dispatcher";
|
import dis from "../../dispatcher/dispatcher";
|
||||||
import { Action } from "../../../dispatcher/actions";
|
import { Action } from "../../dispatcher/actions";
|
||||||
import { Container, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
|
import { WidgetLayoutStore } from "../../stores/widgets/WidgetLayoutStore";
|
||||||
import LegacyCallViewHeader from "./LegacyCallView/LegacyCallViewHeader";
|
import ActiveWidgetStore, { ActiveWidgetStoreEvent } from "../../stores/ActiveWidgetStore";
|
||||||
import ActiveWidgetStore, { ActiveWidgetStoreEvent } from "../../../stores/ActiveWidgetStore";
|
import { ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload";
|
||||||
import WidgetStore, { IApp } from "../../../stores/WidgetStore";
|
import { UPDATE_EVENT } from "../../stores/AsyncStore";
|
||||||
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
|
import { SDKContext, SdkContextClass } from "../../contexts/SDKContext";
|
||||||
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
|
|
||||||
import { SDKContext, SdkContextClass } from "../../../contexts/SDKContext";
|
|
||||||
import { CallStore } from "../../../stores/CallStore";
|
|
||||||
import {
|
import {
|
||||||
useCurrentVoiceBroadcastPreRecording,
|
useCurrentVoiceBroadcastPreRecording,
|
||||||
useCurrentVoiceBroadcastRecording,
|
useCurrentVoiceBroadcastRecording,
|
||||||
@ -46,8 +40,9 @@ import {
|
|||||||
VoiceBroadcastRecording,
|
VoiceBroadcastRecording,
|
||||||
VoiceBroadcastRecordingPip,
|
VoiceBroadcastRecordingPip,
|
||||||
VoiceBroadcastSmallPlaybackBody,
|
VoiceBroadcastSmallPlaybackBody,
|
||||||
} from "../../../voice-broadcast";
|
} from "../../voice-broadcast";
|
||||||
import { useCurrentVoiceBroadcastPlayback } from "../../../voice-broadcast/hooks/useCurrentVoiceBroadcastPlayback";
|
import { useCurrentVoiceBroadcastPlayback } from "../../voice-broadcast/hooks/useCurrentVoiceBroadcastPlayback";
|
||||||
|
import { WidgetPip } from "../views/pips/WidgetPip";
|
||||||
|
|
||||||
const SHOW_CALL_IN_STATES = [
|
const SHOW_CALL_IN_STATES = [
|
||||||
CallState.Connected,
|
CallState.Connected,
|
||||||
@ -59,9 +54,10 @@ const SHOW_CALL_IN_STATES = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
voiceBroadcastRecording?: Optional<VoiceBroadcastRecording>;
|
voiceBroadcastRecording: Optional<VoiceBroadcastRecording>;
|
||||||
voiceBroadcastPreRecording?: Optional<VoiceBroadcastPreRecording>;
|
voiceBroadcastPreRecording: Optional<VoiceBroadcastPreRecording>;
|
||||||
voiceBroadcastPlayback?: Optional<VoiceBroadcastPlayback>;
|
voiceBroadcastPlayback: Optional<VoiceBroadcastPlayback>;
|
||||||
|
movePersistedElement: MutableRefObject<(() => void) | undefined>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
@ -78,20 +74,8 @@ interface IState {
|
|||||||
persistentWidgetId: string;
|
persistentWidgetId: string;
|
||||||
persistentRoomId: string;
|
persistentRoomId: string;
|
||||||
showWidgetInPip: boolean;
|
showWidgetInPip: boolean;
|
||||||
|
|
||||||
moving: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const getRoomAndAppForWidget = (widgetId: string, roomId: string): [Room | null, IApp | null] => {
|
|
||||||
if (!widgetId) return [null, null];
|
|
||||||
if (!roomId) return [null, null];
|
|
||||||
|
|
||||||
const room = MatrixClientPeg.get().getRoom(roomId);
|
|
||||||
const app = WidgetStore.instance.getApps(roomId).find((app) => app.id === widgetId);
|
|
||||||
|
|
||||||
return [room, app || null];
|
|
||||||
};
|
|
||||||
|
|
||||||
// Splits a list of calls into one 'primary' one and a list
|
// Splits a list of calls into one 'primary' one and a list
|
||||||
// (which should be a single element) of other calls.
|
// (which should be a single element) of other calls.
|
||||||
// The primary will be the one not on hold, or an arbitrary one
|
// The primary will be the one not on hold, or an arbitrary one
|
||||||
@ -128,16 +112,12 @@ function getPrimarySecondaryCallsForPip(roomId: Optional<string>): [MatrixCall |
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PipView shows a small version of the LegacyCallView or a sticky widget hovering over the UI in 'picture-in-picture'
|
* PipContainer shows a small version of the LegacyCallView or a sticky widget hovering over the UI in
|
||||||
* (PiP mode). It displays the call(s) which is *not* in the room the user is currently viewing
|
* 'picture-in-picture' (PiP mode). It displays the call(s) which is *not* in the room the user is currently viewing
|
||||||
* and all widgets that are active but not shown in any other possible container.
|
* and all widgets that are active but not shown in any other possible container.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
class PipView extends React.Component<IProps, IState> {
|
class PipContainerInner extends React.Component<IProps, IState> {
|
||||||
// The cast is not so great, but solves the typing issue for the moment.
|
|
||||||
// Proper solution: use useRef (requires the component to be refactored to a functional component).
|
|
||||||
private movePersistedElement = createRef<() => void>() as React.MutableRefObject<() => void>;
|
|
||||||
|
|
||||||
public constructor(props: IProps) {
|
public constructor(props: IProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
@ -146,7 +126,6 @@ class PipView extends React.Component<IProps, IState> {
|
|||||||
const [primaryCall, secondaryCalls] = getPrimarySecondaryCallsForPip(roomId);
|
const [primaryCall, secondaryCalls] = getPrimarySecondaryCallsForPip(roomId);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
moving: false,
|
|
||||||
viewedRoomId: roomId || undefined,
|
viewedRoomId: roomId || undefined,
|
||||||
primaryCall: primaryCall || null,
|
primaryCall: primaryCall || null,
|
||||||
secondaryCall: secondaryCalls[0],
|
secondaryCall: secondaryCalls[0],
|
||||||
@ -168,7 +147,6 @@ class PipView extends React.Component<IProps, IState> {
|
|||||||
ActiveWidgetStore.instance.on(ActiveWidgetStoreEvent.Persistence, this.onWidgetPersistence);
|
ActiveWidgetStore.instance.on(ActiveWidgetStoreEvent.Persistence, this.onWidgetPersistence);
|
||||||
ActiveWidgetStore.instance.on(ActiveWidgetStoreEvent.Dock, this.onWidgetDockChanges);
|
ActiveWidgetStore.instance.on(ActiveWidgetStoreEvent.Dock, this.onWidgetDockChanges);
|
||||||
ActiveWidgetStore.instance.on(ActiveWidgetStoreEvent.Undock, this.onWidgetDockChanges);
|
ActiveWidgetStore.instance.on(ActiveWidgetStoreEvent.Undock, this.onWidgetDockChanges);
|
||||||
document.addEventListener("mouseup", this.onEndMoving.bind(this));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentWillUnmount() {
|
public componentWillUnmount() {
|
||||||
@ -184,18 +162,9 @@ class PipView extends React.Component<IProps, IState> {
|
|||||||
ActiveWidgetStore.instance.off(ActiveWidgetStoreEvent.Persistence, this.onWidgetPersistence);
|
ActiveWidgetStore.instance.off(ActiveWidgetStoreEvent.Persistence, this.onWidgetPersistence);
|
||||||
ActiveWidgetStore.instance.off(ActiveWidgetStoreEvent.Dock, this.onWidgetDockChanges);
|
ActiveWidgetStore.instance.off(ActiveWidgetStoreEvent.Dock, this.onWidgetDockChanges);
|
||||||
ActiveWidgetStore.instance.off(ActiveWidgetStoreEvent.Undock, this.onWidgetDockChanges);
|
ActiveWidgetStore.instance.off(ActiveWidgetStoreEvent.Undock, this.onWidgetDockChanges);
|
||||||
document.removeEventListener("mouseup", this.onEndMoving.bind(this));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private onStartMoving() {
|
private onMove = () => this.props.movePersistedElement.current?.();
|
||||||
this.setState({ moving: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
private onEndMoving() {
|
|
||||||
this.setState({ moving: false });
|
|
||||||
}
|
|
||||||
|
|
||||||
private onMove = () => this.movePersistedElement.current?.();
|
|
||||||
|
|
||||||
private onRoomViewStoreUpdate = () => {
|
private onRoomViewStoreUpdate = () => {
|
||||||
const newRoomId = SdkContextClass.instance.roomViewStore.getRoomId();
|
const newRoomId = SdkContextClass.instance.roomViewStore.getRoomId();
|
||||||
@ -265,53 +234,6 @@ class PipView extends React.Component<IProps, IState> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private onMaximize = (): void => {
|
|
||||||
const widgetId = this.state.persistentWidgetId;
|
|
||||||
const roomId = this.state.persistentRoomId;
|
|
||||||
|
|
||||||
if (this.state.showWidgetInPip && widgetId && roomId) {
|
|
||||||
const [room, app] = getRoomAndAppForWidget(widgetId, roomId);
|
|
||||||
|
|
||||||
if (room && app) {
|
|
||||||
WidgetLayoutStore.instance.moveToContainer(room, app, Container.Center);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dis.dispatch({
|
|
||||||
action: "video_fullscreen",
|
|
||||||
fullscreen: true,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
private onPin = (): void => {
|
|
||||||
if (!this.state.showWidgetInPip) return;
|
|
||||||
|
|
||||||
const [room, app] = getRoomAndAppForWidget(this.state.persistentWidgetId, this.state.persistentRoomId);
|
|
||||||
|
|
||||||
if (room && app) {
|
|
||||||
WidgetLayoutStore.instance.moveToContainer(room, app, Container.Top);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private onExpand = (): void => {
|
|
||||||
const widgetId = this.state.persistentWidgetId;
|
|
||||||
if (!widgetId || !this.state.showWidgetInPip) return;
|
|
||||||
|
|
||||||
dis.dispatch({
|
|
||||||
action: Action.ViewRoom,
|
|
||||||
room_id: this.state.persistentRoomId,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
private onViewCall = (): void =>
|
|
||||||
dis.dispatch<ViewRoomPayload>({
|
|
||||||
action: Action.ViewRoom,
|
|
||||||
room_id: this.state.persistentRoomId,
|
|
||||||
view_call: true,
|
|
||||||
metricsTrigger: undefined,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Accepts a persistentWidgetId to be able to skip awaiting the setState for persistentWidgetId
|
// Accepts a persistentWidgetId to be able to skip awaiting the setState for persistentWidgetId
|
||||||
public updateShowWidgetInPip(
|
public updateShowWidgetInPip(
|
||||||
persistentWidgetId = this.state.persistentWidgetId,
|
persistentWidgetId = this.state.persistentWidgetId,
|
||||||
@ -373,24 +295,20 @@ class PipView extends React.Component<IProps, IState> {
|
|||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
const pipMode = true;
|
const pipMode = true;
|
||||||
let pipContent: CreatePipChildren | null = null;
|
let pipContent: Array<CreatePipChildren> = [];
|
||||||
|
|
||||||
if (this.props.voiceBroadcastPlayback) {
|
|
||||||
pipContent = this.createVoiceBroadcastPlaybackPipContent(this.props.voiceBroadcastPlayback);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.props.voiceBroadcastPreRecording) {
|
|
||||||
pipContent = this.createVoiceBroadcastPreRecordingPipContent(this.props.voiceBroadcastPreRecording);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.props.voiceBroadcastRecording) {
|
if (this.props.voiceBroadcastRecording) {
|
||||||
pipContent = this.createVoiceBroadcastRecordingPipContent(this.props.voiceBroadcastRecording);
|
pipContent = [this.createVoiceBroadcastRecordingPipContent(this.props.voiceBroadcastRecording)];
|
||||||
|
} else if (this.props.voiceBroadcastPreRecording) {
|
||||||
|
pipContent = [this.createVoiceBroadcastPreRecordingPipContent(this.props.voiceBroadcastPreRecording)];
|
||||||
|
} else if (this.props.voiceBroadcastPlayback) {
|
||||||
|
pipContent = [this.createVoiceBroadcastPlaybackPipContent(this.props.voiceBroadcastPlayback)];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.state.primaryCall) {
|
if (this.state.primaryCall) {
|
||||||
// get a ref to call inside the current scope
|
// get a ref to call inside the current scope
|
||||||
const call = this.state.primaryCall;
|
const call = this.state.primaryCall;
|
||||||
pipContent = ({ onStartMoving, onResize }) => (
|
pipContent.push(({ onStartMoving, onResize }) => (
|
||||||
<LegacyCallView
|
<LegacyCallView
|
||||||
onMouseDownOnHeader={onStartMoving}
|
onMouseDownOnHeader={onStartMoving}
|
||||||
call={call}
|
call={call}
|
||||||
@ -398,44 +316,22 @@ class PipView extends React.Component<IProps, IState> {
|
|||||||
pipMode={pipMode}
|
pipMode={pipMode}
|
||||||
onResize={onResize}
|
onResize={onResize}
|
||||||
/>
|
/>
|
||||||
);
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.state.showWidgetInPip) {
|
if (this.state.showWidgetInPip) {
|
||||||
const pipViewClasses = classNames({
|
pipContent.push(({ onStartMoving }) => (
|
||||||
mx_LegacyCallView: true,
|
<WidgetPip
|
||||||
mx_LegacyCallView_pip: pipMode,
|
widgetId={this.state.persistentWidgetId}
|
||||||
mx_LegacyCallView_large: !pipMode,
|
room={MatrixClientPeg.get().getRoom(this.state.persistentRoomId)!}
|
||||||
});
|
viewingRoom={this.state.viewedRoomId === this.state.persistentRoomId}
|
||||||
const roomId = this.state.persistentRoomId;
|
onStartMoving={onStartMoving}
|
||||||
const roomForWidget = MatrixClientPeg.get().getRoom(roomId)!;
|
movePersistedElement={this.props.movePersistedElement}
|
||||||
const viewingCallRoom = this.state.viewedRoomId === roomId;
|
/>
|
||||||
const isCall = CallStore.instance.getActiveCall(roomId) !== null;
|
));
|
||||||
|
|
||||||
pipContent = ({ onStartMoving }) => (
|
|
||||||
<div className={pipViewClasses}>
|
|
||||||
<LegacyCallViewHeader
|
|
||||||
onPipMouseDown={(event) => {
|
|
||||||
onStartMoving?.(event);
|
|
||||||
this.onStartMoving.bind(this)();
|
|
||||||
}}
|
|
||||||
pipMode={pipMode}
|
|
||||||
callRooms={[roomForWidget]}
|
|
||||||
onExpand={!isCall && !viewingCallRoom ? this.onExpand : undefined}
|
|
||||||
onPin={!isCall && viewingCallRoom ? this.onPin : undefined}
|
|
||||||
onMaximize={isCall ? this.onViewCall : viewingCallRoom ? this.onMaximize : undefined}
|
|
||||||
/>
|
|
||||||
<PersistentApp
|
|
||||||
persistentWidgetId={this.state.persistentWidgetId}
|
|
||||||
persistentRoomId={roomId}
|
|
||||||
pointerEvents={this.state.moving ? "none" : undefined}
|
|
||||||
movePersistedElement={this.movePersistedElement}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!!pipContent) {
|
if (pipContent.length) {
|
||||||
return (
|
return (
|
||||||
<PictureInPictureDragger
|
<PictureInPictureDragger
|
||||||
className="mx_LegacyCallPreview"
|
className="mx_LegacyCallPreview"
|
||||||
@ -452,7 +348,7 @@ class PipView extends React.Component<IProps, IState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const PipViewHOC: React.FC<IProps> = (props) => {
|
export const PipContainer: React.FC = () => {
|
||||||
const sdkContext = useContext(SDKContext);
|
const sdkContext = useContext(SDKContext);
|
||||||
const voiceBroadcastPreRecordingStore = sdkContext.voiceBroadcastPreRecordingStore;
|
const voiceBroadcastPreRecordingStore = sdkContext.voiceBroadcastPreRecordingStore;
|
||||||
const { currentVoiceBroadcastPreRecording } = useCurrentVoiceBroadcastPreRecording(voiceBroadcastPreRecordingStore);
|
const { currentVoiceBroadcastPreRecording } = useCurrentVoiceBroadcastPreRecording(voiceBroadcastPreRecordingStore);
|
||||||
@ -463,14 +359,14 @@ const PipViewHOC: React.FC<IProps> = (props) => {
|
|||||||
const voiceBroadcastPlaybacksStore = sdkContext.voiceBroadcastPlaybacksStore;
|
const voiceBroadcastPlaybacksStore = sdkContext.voiceBroadcastPlaybacksStore;
|
||||||
const { currentVoiceBroadcastPlayback } = useCurrentVoiceBroadcastPlayback(voiceBroadcastPlaybacksStore);
|
const { currentVoiceBroadcastPlayback } = useCurrentVoiceBroadcastPlayback(voiceBroadcastPlaybacksStore);
|
||||||
|
|
||||||
|
const movePersistedElement = useRef<() => void>();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PipView
|
<PipContainerInner
|
||||||
voiceBroadcastPlayback={currentVoiceBroadcastPlayback}
|
voiceBroadcastPlayback={currentVoiceBroadcastPlayback}
|
||||||
voiceBroadcastPreRecording={currentVoiceBroadcastPreRecording}
|
voiceBroadcastPreRecording={currentVoiceBroadcastPreRecording}
|
||||||
voiceBroadcastRecording={currentVoiceBroadcastRecording}
|
voiceBroadcastRecording={currentVoiceBroadcastRecording}
|
||||||
{...props}
|
movePersistedElement={movePersistedElement}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default PipViewHOC;
|
|
@ -19,6 +19,7 @@ import { ISearchResults } from "matrix-js-sdk/src/@types/search";
|
|||||||
import { IThreadBundledRelationship } from "matrix-js-sdk/src/models/event";
|
import { IThreadBundledRelationship } from "matrix-js-sdk/src/models/event";
|
||||||
import { THREAD_RELATION_TYPE } from "matrix-js-sdk/src/models/thread";
|
import { THREAD_RELATION_TYPE } from "matrix-js-sdk/src/models/thread";
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||||
|
|
||||||
import ScrollPanel from "./ScrollPanel";
|
import ScrollPanel from "./ScrollPanel";
|
||||||
import { SearchScope } from "../views/rooms/SearchBar";
|
import { SearchScope } from "../views/rooms/SearchBar";
|
||||||
@ -214,6 +215,8 @@ export const RoomSearchView = forwardRef<ScrollPanel, Props>(
|
|||||||
};
|
};
|
||||||
|
|
||||||
let lastRoomId: string;
|
let lastRoomId: string;
|
||||||
|
let mergedTimeline: MatrixEvent[] = [];
|
||||||
|
let ourEventsIndexes: number[] = [];
|
||||||
|
|
||||||
for (let i = (results?.results?.length || 0) - 1; i >= 0; i--) {
|
for (let i = (results?.results?.length || 0) - 1; i >= 0; i--) {
|
||||||
const result = results.results[i];
|
const result = results.results[i];
|
||||||
@ -251,16 +254,54 @@ export const RoomSearchView = forwardRef<ScrollPanel, Props>(
|
|||||||
|
|
||||||
const resultLink = "#/room/" + roomId + "/" + mxEv.getId();
|
const resultLink = "#/room/" + roomId + "/" + mxEv.getId();
|
||||||
|
|
||||||
|
// merging two successive search result if the query is present in both of them
|
||||||
|
const currentTimeline = result.context.getTimeline();
|
||||||
|
const nextTimeline = i > 0 ? results.results[i - 1].context.getTimeline() : [];
|
||||||
|
|
||||||
|
if (i > 0 && currentTimeline[currentTimeline.length - 1].getId() == nextTimeline[0].getId()) {
|
||||||
|
// if this is the first searchResult we merge then add all values of the current searchResult
|
||||||
|
if (mergedTimeline.length == 0) {
|
||||||
|
for (let j = mergedTimeline.length == 0 ? 0 : 1; j < result.context.getTimeline().length; j++) {
|
||||||
|
mergedTimeline.push(currentTimeline[j]);
|
||||||
|
}
|
||||||
|
ourEventsIndexes.push(result.context.getOurEventIndex());
|
||||||
|
}
|
||||||
|
|
||||||
|
// merge the events of the next searchResult
|
||||||
|
for (let j = 1; j < nextTimeline.length; j++) {
|
||||||
|
mergedTimeline.push(nextTimeline[j]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// add the index of the matching event of the next searchResult
|
||||||
|
ourEventsIndexes.push(
|
||||||
|
ourEventsIndexes[ourEventsIndexes.length - 1] +
|
||||||
|
results.results[i - 1].context.getOurEventIndex() +
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mergedTimeline.length == 0) {
|
||||||
|
mergedTimeline = result.context.getTimeline();
|
||||||
|
ourEventsIndexes = [];
|
||||||
|
ourEventsIndexes.push(result.context.getOurEventIndex());
|
||||||
|
}
|
||||||
|
|
||||||
ret.push(
|
ret.push(
|
||||||
<SearchResultTile
|
<SearchResultTile
|
||||||
key={mxEv.getId()}
|
key={mxEv.getId()}
|
||||||
searchResult={result}
|
timeline={mergedTimeline}
|
||||||
searchHighlights={highlights}
|
ourEventsIndexes={ourEventsIndexes}
|
||||||
|
searchHighlights={highlights ?? []}
|
||||||
resultLink={resultLink}
|
resultLink={resultLink}
|
||||||
permalinkCreator={permalinkCreator}
|
permalinkCreator={permalinkCreator}
|
||||||
onHeightChanged={onHeightChanged}
|
onHeightChanged={onHeightChanged}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
ourEventsIndexes = [];
|
||||||
|
mergedTimeline = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -1637,7 +1637,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
|
|||||||
let i = events.length - 1;
|
let i = events.length - 1;
|
||||||
let userMembership = "leave";
|
let userMembership = "leave";
|
||||||
for (; i >= 0; i--) {
|
for (; i >= 0; i--) {
|
||||||
const timeline = room.getTimelineForEvent(events[i].getId());
|
const timeline = this.props.timelineSet.getTimelineForEvent(events[i].getId()!);
|
||||||
if (!timeline) {
|
if (!timeline) {
|
||||||
// Somehow, it seems to be possible for live events to not have
|
// Somehow, it seems to be possible for live events to not have
|
||||||
// a timeline, even though that should not happen. :(
|
// a timeline, even though that should not happen. :(
|
||||||
|
@ -50,11 +50,8 @@ import UserIdentifierCustomisations from "../../customisations/UserIdentifier";
|
|||||||
import PosthogTrackers from "../../PosthogTrackers";
|
import PosthogTrackers from "../../PosthogTrackers";
|
||||||
import { ViewHomePagePayload } from "../../dispatcher/payloads/ViewHomePagePayload";
|
import { ViewHomePagePayload } from "../../dispatcher/payloads/ViewHomePagePayload";
|
||||||
import { Icon as LiveIcon } from "../../../res/img/element-icons/live.svg";
|
import { Icon as LiveIcon } from "../../../res/img/element-icons/live.svg";
|
||||||
import {
|
import { VoiceBroadcastRecording, VoiceBroadcastRecordingsStoreEvent } from "../../voice-broadcast";
|
||||||
VoiceBroadcastRecording,
|
import { SDKContext } from "../../contexts/SDKContext";
|
||||||
VoiceBroadcastRecordingsStore,
|
|
||||||
VoiceBroadcastRecordingsStoreEvent,
|
|
||||||
} from "../../voice-broadcast";
|
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
isPanelCollapsed: boolean;
|
isPanelCollapsed: boolean;
|
||||||
@ -87,21 +84,24 @@ const below = (rect: PartialDOMRect) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default class UserMenu extends React.Component<IProps, IState> {
|
export default class UserMenu extends React.Component<IProps, IState> {
|
||||||
|
public static contextType = SDKContext;
|
||||||
|
public context!: React.ContextType<typeof SDKContext>;
|
||||||
|
|
||||||
private dispatcherRef: string;
|
private dispatcherRef: string;
|
||||||
private themeWatcherRef: string;
|
private themeWatcherRef: string;
|
||||||
private readonly dndWatcherRef: string;
|
private readonly dndWatcherRef: string;
|
||||||
private buttonRef: React.RefObject<HTMLButtonElement> = createRef();
|
private buttonRef: React.RefObject<HTMLButtonElement> = createRef();
|
||||||
private voiceBroadcastRecordingStore = VoiceBroadcastRecordingsStore.instance();
|
|
||||||
|
|
||||||
public constructor(props: IProps) {
|
public constructor(props: IProps, context: React.ContextType<typeof SDKContext>) {
|
||||||
super(props);
|
super(props, context);
|
||||||
|
|
||||||
|
this.context = context;
|
||||||
this.state = {
|
this.state = {
|
||||||
contextMenuPosition: null,
|
contextMenuPosition: null,
|
||||||
isDarkTheme: this.isUserOnDarkTheme(),
|
isDarkTheme: this.isUserOnDarkTheme(),
|
||||||
isHighContrast: this.isUserOnHighContrastTheme(),
|
isHighContrast: this.isUserOnHighContrastTheme(),
|
||||||
selectedSpace: SpaceStore.instance.activeSpaceRoom,
|
selectedSpace: SpaceStore.instance.activeSpaceRoom,
|
||||||
showLiveAvatarAddon: this.voiceBroadcastRecordingStore.hasCurrent(),
|
showLiveAvatarAddon: this.context.voiceBroadcastRecordingsStore.hasCurrent(),
|
||||||
};
|
};
|
||||||
|
|
||||||
OwnProfileStore.instance.on(UPDATE_EVENT, this.onProfileUpdate);
|
OwnProfileStore.instance.on(UPDATE_EVENT, this.onProfileUpdate);
|
||||||
@ -119,7 +119,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
public componentDidMount() {
|
public componentDidMount() {
|
||||||
this.voiceBroadcastRecordingStore.on(
|
this.context.voiceBroadcastRecordingsStore.on(
|
||||||
VoiceBroadcastRecordingsStoreEvent.CurrentChanged,
|
VoiceBroadcastRecordingsStoreEvent.CurrentChanged,
|
||||||
this.onCurrentVoiceBroadcastRecordingChanged,
|
this.onCurrentVoiceBroadcastRecordingChanged,
|
||||||
);
|
);
|
||||||
@ -133,7 +133,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
|||||||
if (this.dispatcherRef) defaultDispatcher.unregister(this.dispatcherRef);
|
if (this.dispatcherRef) defaultDispatcher.unregister(this.dispatcherRef);
|
||||||
OwnProfileStore.instance.off(UPDATE_EVENT, this.onProfileUpdate);
|
OwnProfileStore.instance.off(UPDATE_EVENT, this.onProfileUpdate);
|
||||||
SpaceStore.instance.off(UPDATE_SELECTED_SPACE, this.onSelectedSpaceUpdate);
|
SpaceStore.instance.off(UPDATE_SELECTED_SPACE, this.onSelectedSpaceUpdate);
|
||||||
this.voiceBroadcastRecordingStore.off(
|
this.context.voiceBroadcastRecordingsStore.off(
|
||||||
VoiceBroadcastRecordingsStoreEvent.CurrentChanged,
|
VoiceBroadcastRecordingsStoreEvent.CurrentChanged,
|
||||||
this.onCurrentVoiceBroadcastRecordingChanged,
|
this.onCurrentVoiceBroadcastRecordingChanged,
|
||||||
);
|
);
|
||||||
|
@ -22,7 +22,7 @@ import EmailField from "../../../views/auth/EmailField";
|
|||||||
import { ErrorMessage } from "../../ErrorMessage";
|
import { ErrorMessage } from "../../ErrorMessage";
|
||||||
import Spinner from "../../../views/elements/Spinner";
|
import Spinner from "../../../views/elements/Spinner";
|
||||||
import Field from "../../../views/elements/Field";
|
import Field from "../../../views/elements/Field";
|
||||||
import AccessibleButton from "../../../views/elements/AccessibleButton";
|
import AccessibleButton, { ButtonEvent } from "../../../views/elements/AccessibleButton";
|
||||||
|
|
||||||
interface EnterEmailProps {
|
interface EnterEmailProps {
|
||||||
email: string;
|
email: string;
|
||||||
@ -94,7 +94,10 @@ export const EnterEmail: React.FC<EnterEmailProps> = ({
|
|||||||
className="mx_AuthBody_sign-in-instead-button"
|
className="mx_AuthBody_sign-in-instead-button"
|
||||||
element="button"
|
element="button"
|
||||||
kind="link"
|
kind="link"
|
||||||
onClick={onLoginClick}
|
onClick={(e: ButtonEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
onLoginClick();
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{_t("Sign in instead")}
|
{_t("Sign in instead")}
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
|
@ -60,7 +60,7 @@ export default class SeekBar extends React.PureComponent<IProps, IState> {
|
|||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
percentage: 0,
|
percentage: percentageOf(this.props.playback.timeSeconds, 0, this.props.playback.durationSeconds),
|
||||||
};
|
};
|
||||||
|
|
||||||
// We don't need to de-register: the class handles this for us internally
|
// We don't need to de-register: the class handles this for us internally
|
||||||
|
@ -29,6 +29,7 @@ import * as Avatar from "../../../Avatar";
|
|||||||
import DMRoomMap from "../../../utils/DMRoomMap";
|
import DMRoomMap from "../../../utils/DMRoomMap";
|
||||||
import { mediaFromMxc } from "../../../customisations/Media";
|
import { mediaFromMxc } from "../../../customisations/Media";
|
||||||
import { IOOBData } from "../../../stores/ThreepidInviteStore";
|
import { IOOBData } from "../../../stores/ThreepidInviteStore";
|
||||||
|
import { LocalRoom } from "../../../models/LocalRoom";
|
||||||
|
|
||||||
interface IProps extends Omit<ComponentProps<typeof BaseAvatar>, "name" | "idName" | "url" | "onClick"> {
|
interface IProps extends Omit<ComponentProps<typeof BaseAvatar>, "name" | "idName" | "url" | "onClick"> {
|
||||||
// Room may be left unset here, but if it is,
|
// Room may be left unset here, but if it is,
|
||||||
@ -117,13 +118,26 @@ export default class RoomAvatar extends React.Component<IProps, IState> {
|
|||||||
Modal.createDialog(ImageView, params, "mx_Dialog_lightbox", null, true);
|
Modal.createDialog(ImageView, params, "mx_Dialog_lightbox", null, true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private get roomIdName(): string | undefined {
|
||||||
|
const room = this.props.room;
|
||||||
|
|
||||||
|
if (room) {
|
||||||
|
const dmMapUserId = DMRoomMap.shared().getUserIdForRoomId(room.roomId);
|
||||||
|
// If the room is a DM, we use the other user's ID for the color hash
|
||||||
|
// in order to match the room avatar with their avatar
|
||||||
|
if (dmMapUserId) return dmMapUserId;
|
||||||
|
|
||||||
|
if (room instanceof LocalRoom && room.targets.length === 1) {
|
||||||
|
return room.targets[0].userId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.props.room?.roomId || this.props.oobData?.roomId;
|
||||||
|
}
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
const { room, oobData, viewAvatarOnClick, onClick, className, ...otherProps } = this.props;
|
const { room, oobData, viewAvatarOnClick, onClick, className, ...otherProps } = this.props;
|
||||||
|
|
||||||
const roomName = room?.name ?? oobData.name;
|
const roomName = room?.name ?? oobData.name;
|
||||||
// If the room is a DM, we use the other user's ID for the color hash
|
|
||||||
// in order to match the room avatar with their avatar
|
|
||||||
const idName = room ? DMRoomMap.shared().getUserIdForRoomId(room.roomId) ?? room.roomId : oobData.roomId;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseAvatar
|
<BaseAvatar
|
||||||
@ -132,7 +146,7 @@ export default class RoomAvatar extends React.Component<IProps, IState> {
|
|||||||
mx_RoomAvatar_isSpaceRoom: (room?.getType() ?? this.props.oobData?.roomType) === RoomType.Space,
|
mx_RoomAvatar_isSpaceRoom: (room?.getType() ?? this.props.oobData?.roomType) === RoomType.Space,
|
||||||
})}
|
})}
|
||||||
name={roomName}
|
name={roomName}
|
||||||
idName={idName}
|
idName={this.roomIdName}
|
||||||
urls={this.state.urls}
|
urls={this.state.urls}
|
||||||
onClick={viewAvatarOnClick && this.state.urls[0] ? this.onRoomAvatarClick : onClick}
|
onClick={viewAvatarOnClick && this.state.urls[0] ? this.onRoomAvatarClick : onClick}
|
||||||
/>
|
/>
|
||||||
|
@ -45,10 +45,12 @@ const BeaconMarker: React.FC<Props> = ({ map, beacon, tooltip }) => {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const geoUri = latestLocationState?.uri;
|
const geoUri = latestLocationState.uri || "";
|
||||||
|
|
||||||
const markerRoomMember =
|
const assetTypeIsSelf = beacon.beaconInfo?.assetType === LocationAssetType.Self;
|
||||||
beacon.beaconInfo.assetType === LocationAssetType.Self ? room.getMember(beacon.beaconInfoOwner) : undefined;
|
const _member = room?.getMember(beacon.beaconInfoOwner);
|
||||||
|
|
||||||
|
const markerRoomMember = assetTypeIsSelf && _member ? _member : undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SmartMarker
|
<SmartMarker
|
||||||
|
@ -103,7 +103,7 @@ const RoomLiveShareWarningInner: React.FC<RoomLiveShareWarningInnerProps> = ({ l
|
|||||||
|
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
className="mx_RoomLiveShareWarning_stopButton"
|
className="mx_RoomLiveShareWarning_stopButton"
|
||||||
data-test-id="room-live-share-primary-button"
|
data-testid="room-live-share-primary-button"
|
||||||
onClick={stopPropagationWrapper(onButtonClick)}
|
onClick={stopPropagationWrapper(onButtonClick)}
|
||||||
kind="danger"
|
kind="danger"
|
||||||
element="button"
|
element="button"
|
||||||
@ -113,7 +113,7 @@ const RoomLiveShareWarningInner: React.FC<RoomLiveShareWarningInnerProps> = ({ l
|
|||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
{hasLocationPublishError && (
|
{hasLocationPublishError && (
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
data-test-id="room-live-share-wire-error-close-button"
|
data-testid="room-live-share-wire-error-close-button"
|
||||||
title={_t("Stop and close")}
|
title={_t("Stop and close")}
|
||||||
element="button"
|
element="button"
|
||||||
className="mx_RoomLiveShareWarning_closeButton"
|
className="mx_RoomLiveShareWarning_closeButton"
|
||||||
|
@ -23,11 +23,14 @@ import RoomListActions from "../../../actions/RoomListActions";
|
|||||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||||
import dis from "../../../dispatcher/dispatcher";
|
import dis from "../../../dispatcher/dispatcher";
|
||||||
import { useEventEmitterState } from "../../../hooks/useEventEmitter";
|
import { useEventEmitterState } from "../../../hooks/useEventEmitter";
|
||||||
|
import { useUnreadNotifications } from "../../../hooks/useUnreadNotifications";
|
||||||
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
|
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
|
import { NotificationColor } from "../../../stores/notifications/NotificationColor";
|
||||||
import { DefaultTagID, TagID } from "../../../stores/room-list/models";
|
import { DefaultTagID, TagID } from "../../../stores/room-list/models";
|
||||||
import RoomListStore, { LISTS_UPDATE_EVENT } from "../../../stores/room-list/RoomListStore";
|
import RoomListStore, { LISTS_UPDATE_EVENT } from "../../../stores/room-list/RoomListStore";
|
||||||
import DMRoomMap from "../../../utils/DMRoomMap";
|
import DMRoomMap from "../../../utils/DMRoomMap";
|
||||||
|
import { clearRoomNotification } from "../../../utils/notifications";
|
||||||
import { IProps as IContextMenuProps } from "../../structures/ContextMenu";
|
import { IProps as IContextMenuProps } from "../../structures/ContextMenu";
|
||||||
import IconizedContextMenu, {
|
import IconizedContextMenu, {
|
||||||
IconizedContextMenuCheckbox,
|
IconizedContextMenuCheckbox,
|
||||||
@ -36,7 +39,7 @@ import IconizedContextMenu, {
|
|||||||
} from "../context_menus/IconizedContextMenu";
|
} from "../context_menus/IconizedContextMenu";
|
||||||
import { ButtonEvent } from "../elements/AccessibleButton";
|
import { ButtonEvent } from "../elements/AccessibleButton";
|
||||||
|
|
||||||
interface IProps extends IContextMenuProps {
|
export interface RoomGeneralContextMenuProps extends IContextMenuProps {
|
||||||
room: Room;
|
room: Room;
|
||||||
onPostFavoriteClick?: (event: ButtonEvent) => void;
|
onPostFavoriteClick?: (event: ButtonEvent) => void;
|
||||||
onPostLowPriorityClick?: (event: ButtonEvent) => void;
|
onPostLowPriorityClick?: (event: ButtonEvent) => void;
|
||||||
@ -58,7 +61,7 @@ export const RoomGeneralContextMenu = ({
|
|||||||
onPostLeaveClick,
|
onPostLeaveClick,
|
||||||
onPostForgetClick,
|
onPostForgetClick,
|
||||||
...props
|
...props
|
||||||
}: IProps) => {
|
}: RoomGeneralContextMenuProps) => {
|
||||||
const cli = useContext(MatrixClientContext);
|
const cli = useContext(MatrixClientContext);
|
||||||
const roomTags = useEventEmitterState(RoomListStore.instance, LISTS_UPDATE_EVENT, () =>
|
const roomTags = useEventEmitterState(RoomListStore.instance, LISTS_UPDATE_EVENT, () =>
|
||||||
RoomListStore.instance.getTagsForRoom(room),
|
RoomListStore.instance.getTagsForRoom(room),
|
||||||
@ -115,8 +118,8 @@ export const RoomGeneralContextMenu = ({
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
let inviteOption: JSX.Element;
|
let inviteOption: JSX.Element | null = null;
|
||||||
if (room.canInvite(cli.getUserId()) && !isDm) {
|
if (room.canInvite(cli.getUserId()!) && !isDm) {
|
||||||
inviteOption = (
|
inviteOption = (
|
||||||
<IconizedContextMenuOption
|
<IconizedContextMenuOption
|
||||||
onClick={wrapHandler(
|
onClick={wrapHandler(
|
||||||
@ -133,7 +136,7 @@ export const RoomGeneralContextMenu = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let copyLinkOption: JSX.Element;
|
let copyLinkOption: JSX.Element | null = null;
|
||||||
if (!isDm) {
|
if (!isDm) {
|
||||||
copyLinkOption = (
|
copyLinkOption = (
|
||||||
<IconizedContextMenuOption
|
<IconizedContextMenuOption
|
||||||
@ -201,17 +204,34 @@ export const RoomGeneralContextMenu = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { color } = useUnreadNotifications(room);
|
||||||
|
const markAsReadOption: JSX.Element | null =
|
||||||
|
color > NotificationColor.None ? (
|
||||||
|
<IconizedContextMenuCheckbox
|
||||||
|
onClick={() => {
|
||||||
|
clearRoomNotification(room, cli);
|
||||||
|
onFinished?.();
|
||||||
|
}}
|
||||||
|
active={false}
|
||||||
|
label={_t("Mark as read")}
|
||||||
|
iconClassName="mx_RoomGeneralContextMenu_iconMarkAsRead"
|
||||||
|
/>
|
||||||
|
) : null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<IconizedContextMenu {...props} onFinished={onFinished} className="mx_RoomGeneralContextMenu" compact>
|
<IconizedContextMenu {...props} onFinished={onFinished} className="mx_RoomGeneralContextMenu" compact>
|
||||||
{!roomTags.includes(DefaultTagID.Archived) && (
|
<IconizedContextMenuOptionList>
|
||||||
<IconizedContextMenuOptionList>
|
{markAsReadOption}
|
||||||
{favoriteOption}
|
{!roomTags.includes(DefaultTagID.Archived) && (
|
||||||
{lowPriorityOption}
|
<>
|
||||||
{inviteOption}
|
{favoriteOption}
|
||||||
{copyLinkOption}
|
{lowPriorityOption}
|
||||||
{settingsOption}
|
{inviteOption}
|
||||||
</IconizedContextMenuOptionList>
|
{copyLinkOption}
|
||||||
)}
|
{settingsOption}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</IconizedContextMenuOptionList>
|
||||||
<IconizedContextMenuOptionList red>{leaveOption}</IconizedContextMenuOptionList>
|
<IconizedContextMenuOptionList red>{leaveOption}</IconizedContextMenuOptionList>
|
||||||
</IconizedContextMenu>
|
</IconizedContextMenu>
|
||||||
);
|
);
|
||||||
|
@ -14,12 +14,14 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
import { Feature, ServerSupport } from "matrix-js-sdk/src/feature";
|
||||||
|
import { MatrixEvent, RelationType } from "matrix-js-sdk/src/matrix";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||||
import Modal from "../../../Modal";
|
import Modal from "../../../Modal";
|
||||||
|
import { isVoiceBroadcastStartedEvent } from "../../../voice-broadcast/utils/isVoiceBroadcastStartedEvent";
|
||||||
import ErrorDialog from "./ErrorDialog";
|
import ErrorDialog from "./ErrorDialog";
|
||||||
import TextInputDialog from "./TextInputDialog";
|
import TextInputDialog from "./TextInputDialog";
|
||||||
|
|
||||||
@ -55,6 +57,14 @@ export function createRedactEventDialog({
|
|||||||
mxEvent: MatrixEvent;
|
mxEvent: MatrixEvent;
|
||||||
onCloseDialog?: () => void;
|
onCloseDialog?: () => void;
|
||||||
}) {
|
}) {
|
||||||
|
const eventId = mxEvent.getId();
|
||||||
|
|
||||||
|
if (!eventId) throw new Error("cannot redact event without ID");
|
||||||
|
|
||||||
|
const roomId = mxEvent.getRoomId();
|
||||||
|
|
||||||
|
if (!roomId) throw new Error(`cannot redact event ${mxEvent.getId()} without room ID`);
|
||||||
|
|
||||||
Modal.createDialog(
|
Modal.createDialog(
|
||||||
ConfirmRedactDialog,
|
ConfirmRedactDialog,
|
||||||
{
|
{
|
||||||
@ -62,10 +72,27 @@ export function createRedactEventDialog({
|
|||||||
if (!proceed) return;
|
if (!proceed) return;
|
||||||
|
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
|
const withRelations: { with_relations?: RelationType[] } = {};
|
||||||
|
|
||||||
|
// redact related events if this is a voice broadcast started event and
|
||||||
|
// server has support for relation based redactions
|
||||||
|
if (isVoiceBroadcastStartedEvent(mxEvent)) {
|
||||||
|
const relationBasedRedactionsSupport = cli.canSupport.get(Feature.RelationBasedRedactions);
|
||||||
|
if (
|
||||||
|
relationBasedRedactionsSupport &&
|
||||||
|
relationBasedRedactionsSupport !== ServerSupport.Unsupported
|
||||||
|
) {
|
||||||
|
withRelations.with_relations = [RelationType.Reference];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
onCloseDialog?.();
|
onCloseDialog?.();
|
||||||
await cli.redactEvent(mxEvent.getRoomId(), mxEvent.getId(), undefined, reason ? { reason } : {});
|
await cli.redactEvent(roomId, eventId, undefined, {
|
||||||
} catch (e) {
|
...(reason ? { reason } : {}),
|
||||||
|
...withRelations,
|
||||||
|
});
|
||||||
|
} catch (e: any) {
|
||||||
const code = e.errcode || e.statusCode;
|
const code = e.errcode || e.statusCode;
|
||||||
// only show the dialog if failing for something other than a network error
|
// only show the dialog if failing for something other than a network error
|
||||||
// (e.g. no errcode or statusCode) as in that case the redactions end up in the
|
// (e.g. no errcode or statusCode) as in that case the redactions end up in the
|
||||||
|
@ -31,6 +31,7 @@ import { AccountDataExplorer, RoomAccountDataExplorer } from "./devtools/Account
|
|||||||
import SettingsFlag from "../elements/SettingsFlag";
|
import SettingsFlag from "../elements/SettingsFlag";
|
||||||
import { SettingLevel } from "../../../settings/SettingLevel";
|
import { SettingLevel } from "../../../settings/SettingLevel";
|
||||||
import ServerInfo from "./devtools/ServerInfo";
|
import ServerInfo from "./devtools/ServerInfo";
|
||||||
|
import { Features } from "../../../settings/Settings";
|
||||||
|
|
||||||
enum Category {
|
enum Category {
|
||||||
Room,
|
Room,
|
||||||
@ -105,6 +106,7 @@ const DevtoolsDialog: React.FC<IProps> = ({ roomId, onFinished }) => {
|
|||||||
<SettingsFlag name="developerMode" level={SettingLevel.ACCOUNT} />
|
<SettingsFlag name="developerMode" level={SettingLevel.ACCOUNT} />
|
||||||
<SettingsFlag name="showHiddenEventsInTimeline" level={SettingLevel.DEVICE} />
|
<SettingsFlag name="showHiddenEventsInTimeline" level={SettingLevel.DEVICE} />
|
||||||
<SettingsFlag name="enableWidgetScreenshots" level={SettingLevel.ACCOUNT} />
|
<SettingsFlag name="enableWidgetScreenshots" level={SettingLevel.ACCOUNT} />
|
||||||
|
<SettingsFlag name={Features.VoiceBroadcastForceSmallChunks} level={SettingLevel.DEVICE} />
|
||||||
</div>
|
</div>
|
||||||
</BaseTool>
|
</BaseTool>
|
||||||
);
|
);
|
||||||
|
@ -85,7 +85,7 @@ interface IProps {
|
|||||||
widgetPageTitle?: string;
|
widgetPageTitle?: string;
|
||||||
showLayoutButtons?: boolean;
|
showLayoutButtons?: boolean;
|
||||||
// Handle to manually notify the PersistedElement that it needs to move
|
// Handle to manually notify the PersistedElement that it needs to move
|
||||||
movePersistedElement?: MutableRefObject<() => void>;
|
movePersistedElement?: MutableRefObject<(() => void) | undefined>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
|
@ -16,7 +16,6 @@ limitations under the License.
|
|||||||
|
|
||||||
import React, { MutableRefObject } from "react";
|
import React, { MutableRefObject } from "react";
|
||||||
import ReactDOM from "react-dom";
|
import ReactDOM from "react-dom";
|
||||||
import { throttle } from "lodash";
|
|
||||||
import { isNullOrUndefined } from "matrix-js-sdk/src/utils";
|
import { isNullOrUndefined } from "matrix-js-sdk/src/utils";
|
||||||
|
|
||||||
import dis from "../../../dispatcher/dispatcher";
|
import dis from "../../../dispatcher/dispatcher";
|
||||||
@ -58,7 +57,7 @@ interface IProps {
|
|||||||
style?: React.StyleHTMLAttributes<HTMLDivElement>;
|
style?: React.StyleHTMLAttributes<HTMLDivElement>;
|
||||||
|
|
||||||
// Handle to manually notify this PersistedElement that it needs to move
|
// Handle to manually notify this PersistedElement that it needs to move
|
||||||
moveRef?: MutableRefObject<() => void>;
|
moveRef?: MutableRefObject<(() => void) | undefined>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -177,24 +176,20 @@ export default class PersistedElement extends React.Component<IProps> {
|
|||||||
child.style.display = visible ? "block" : "none";
|
child.style.display = visible ? "block" : "none";
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateChildPosition = throttle(
|
private updateChildPosition(child: HTMLDivElement, parent: HTMLDivElement): void {
|
||||||
(child: HTMLDivElement, parent: HTMLDivElement): void => {
|
if (!child || !parent) return;
|
||||||
if (!child || !parent) return;
|
|
||||||
|
|
||||||
const parentRect = parent.getBoundingClientRect();
|
const parentRect = parent.getBoundingClientRect();
|
||||||
Object.assign(child.style, {
|
Object.assign(child.style, {
|
||||||
zIndex: isNullOrUndefined(this.props.zIndex) ? 9 : this.props.zIndex,
|
zIndex: isNullOrUndefined(this.props.zIndex) ? 9 : this.props.zIndex,
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
top: "0",
|
top: "0",
|
||||||
left: "0",
|
left: "0",
|
||||||
transform: `translateX(${parentRect.left}px) translateY(${parentRect.top}px)`,
|
transform: `translateX(${parentRect.left}px) translateY(${parentRect.top}px)`,
|
||||||
width: parentRect.width + "px",
|
width: parentRect.width + "px",
|
||||||
height: parentRect.height + "px",
|
height: parentRect.height + "px",
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
16,
|
|
||||||
{ trailing: true, leading: true },
|
|
||||||
);
|
|
||||||
|
|
||||||
public render(): JSX.Element {
|
public render(): JSX.Element {
|
||||||
return <div ref={this.collectChildContainer} />;
|
return <div ref={this.collectChildContainer} />;
|
||||||
|
@ -27,7 +27,7 @@ interface IProps {
|
|||||||
persistentWidgetId: string;
|
persistentWidgetId: string;
|
||||||
persistentRoomId: string;
|
persistentRoomId: string;
|
||||||
pointerEvents?: string;
|
pointerEvents?: string;
|
||||||
movePersistedElement: MutableRefObject<() => void>;
|
movePersistedElement: MutableRefObject<(() => void) | undefined>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class PersistentApp extends React.Component<IProps> {
|
export default class PersistentApp extends React.Component<IProps> {
|
||||||
|
@ -138,7 +138,7 @@ export default class PollCreateDialog extends ScrollableBaseModal<IProps, IState
|
|||||||
const pollStart = PollStartEvent.from(
|
const pollStart = PollStartEvent.from(
|
||||||
this.state.question.trim(),
|
this.state.question.trim(),
|
||||||
this.state.options.map((a) => a.trim()).filter((a) => !!a),
|
this.state.options.map((a) => a.trim()).filter((a) => !!a),
|
||||||
this.state.kind,
|
this.state.kind.name,
|
||||||
).serialize();
|
).serialize();
|
||||||
|
|
||||||
if (!this.props.editingMxEvent) {
|
if (!this.props.editingMxEvent) {
|
||||||
|
@ -66,28 +66,30 @@ const ActiveCallEvent = forwardRef<any, ActiveCallEventProps>(
|
|||||||
width={24}
|
width={24}
|
||||||
height={24}
|
height={24}
|
||||||
/>
|
/>
|
||||||
<div className="mx_CallEvent_infoRows">
|
<div className="mx_CallEvent_columns">
|
||||||
<span className="mx_CallEvent_title">
|
<div className="mx_CallEvent_details">
|
||||||
{_t("%(name)s started a video call", { name: senderName })}
|
<span className="mx_CallEvent_title">
|
||||||
</span>
|
{_t("%(name)s started a video call", { name: senderName })}
|
||||||
<LiveContentSummary
|
</span>
|
||||||
type={LiveContentType.Video}
|
<LiveContentSummary
|
||||||
text={_t("Video call")}
|
type={LiveContentType.Video}
|
||||||
active={false}
|
text={_t("Video call")}
|
||||||
participantCount={participatingMembers.length}
|
active={false}
|
||||||
/>
|
participantCount={participatingMembers.length}
|
||||||
<FacePile members={facePileMembers} faceSize={24} overflow={facePileOverflow} />
|
/>
|
||||||
|
<FacePile members={facePileMembers} faceSize={24} overflow={facePileOverflow} />
|
||||||
|
</div>
|
||||||
|
{call && <GroupCallDuration groupCall={call.groupCall} />}
|
||||||
|
<AccessibleTooltipButton
|
||||||
|
className="mx_CallEvent_button"
|
||||||
|
kind={buttonKind}
|
||||||
|
disabled={onButtonClick === null || buttonDisabledTooltip !== undefined}
|
||||||
|
onClick={onButtonClick}
|
||||||
|
tooltip={buttonDisabledTooltip}
|
||||||
|
>
|
||||||
|
{buttonText}
|
||||||
|
</AccessibleTooltipButton>
|
||||||
</div>
|
</div>
|
||||||
{call && <GroupCallDuration groupCall={call.groupCall} />}
|
|
||||||
<AccessibleTooltipButton
|
|
||||||
className="mx_CallEvent_button"
|
|
||||||
kind={buttonKind}
|
|
||||||
disabled={onButtonClick === null || buttonDisabledTooltip !== undefined}
|
|
||||||
onClick={onButtonClick}
|
|
||||||
tooltip={buttonDisabledTooltip}
|
|
||||||
>
|
|
||||||
{buttonText}
|
|
||||||
</AccessibleTooltipButton>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -164,15 +166,17 @@ export const CallEvent = forwardRef<any, CallEventProps>(({ mxEvent }, ref) => {
|
|||||||
const call = useCall(mxEvent.getRoomId()!);
|
const call = useCall(mxEvent.getRoomId()!);
|
||||||
const latestEvent = client
|
const latestEvent = client
|
||||||
.getRoom(mxEvent.getRoomId())!
|
.getRoom(mxEvent.getRoomId())!
|
||||||
.currentState.getStateEvents(mxEvent.getType(), mxEvent.getStateKey()!);
|
.currentState.getStateEvents(mxEvent.getType(), mxEvent.getStateKey()!)!;
|
||||||
|
|
||||||
if ("m.terminated" in latestEvent.getContent()) {
|
if ("m.terminated" in latestEvent.getContent()) {
|
||||||
// The call is terminated
|
// The call is terminated
|
||||||
return (
|
return (
|
||||||
<div className="mx_CallEvent_wrapper" ref={ref}>
|
<div className="mx_CallEvent_wrapper" ref={ref}>
|
||||||
<div className="mx_CallEvent mx_CallEvent_inactive">
|
<div className="mx_CallEvent mx_CallEvent_inactive">
|
||||||
<span className="mx_CallEvent_title">{_t("Video call ended")}</span>
|
<div className="mx_CallEvent_columns">
|
||||||
<CallDuration delta={latestEvent.getTs() - mxEvent.getTs()} />
|
<span className="mx_CallEvent_title">{_t("Video call ended")}</span>
|
||||||
|
<CallDuration delta={latestEvent.getTs() - mxEvent.getTs()} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -59,6 +59,7 @@ import { Action } from "../../../dispatcher/actions";
|
|||||||
import { ShowThreadPayload } from "../../../dispatcher/payloads/ShowThreadPayload";
|
import { ShowThreadPayload } from "../../../dispatcher/payloads/ShowThreadPayload";
|
||||||
import useFavouriteMessages from "../../../hooks/useFavouriteMessages";
|
import useFavouriteMessages from "../../../hooks/useFavouriteMessages";
|
||||||
import { GetRelationsForEvent } from "../rooms/EventTile";
|
import { GetRelationsForEvent } from "../rooms/EventTile";
|
||||||
|
import { VoiceBroadcastInfoEventType } from "../../../voice-broadcast/types";
|
||||||
|
|
||||||
interface IOptionsButtonProps {
|
interface IOptionsButtonProps {
|
||||||
mxEvent: MatrixEvent;
|
mxEvent: MatrixEvent;
|
||||||
@ -394,7 +395,8 @@ export default class MessageActionBar extends React.PureComponent<IMessageAction
|
|||||||
* until cross-platform support
|
* until cross-platform support
|
||||||
* (PSF-1041)
|
* (PSF-1041)
|
||||||
*/
|
*/
|
||||||
!M_BEACON_INFO.matches(this.props.mxEvent.getType());
|
!M_BEACON_INFO.matches(this.props.mxEvent.getType()) &&
|
||||||
|
!(this.props.mxEvent.getType() === VoiceBroadcastInfoEventType);
|
||||||
|
|
||||||
return inNotThreadTimeline && isAllowedMessageType;
|
return inNotThreadTimeline && isAllowedMessageType;
|
||||||
}
|
}
|
||||||
|
140
src/components/views/pips/WidgetPip.tsx
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { FC, MutableRefObject, useCallback, useMemo } from "react";
|
||||||
|
import { Room, RoomEvent } from "matrix-js-sdk/src/models/room";
|
||||||
|
|
||||||
|
import PersistentApp from "../elements/PersistentApp";
|
||||||
|
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
||||||
|
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
|
||||||
|
import { Action } from "../../../dispatcher/actions";
|
||||||
|
import { useCallForWidget } from "../../../hooks/useCall";
|
||||||
|
import WidgetStore from "../../../stores/WidgetStore";
|
||||||
|
import { Container, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
|
||||||
|
import { useTypedEventEmitterState } from "../../../hooks/useEventEmitter";
|
||||||
|
import Toolbar from "../../../accessibility/Toolbar";
|
||||||
|
import { RovingAccessibleButton, RovingAccessibleTooltipButton } from "../../../accessibility/RovingTabIndex";
|
||||||
|
import { Icon as BackIcon } from "../../../../res/img/element-icons/back.svg";
|
||||||
|
import { Icon as HangupIcon } from "../../../../res/img/element-icons/call/hangup.svg";
|
||||||
|
import { _t } from "../../../languageHandler";
|
||||||
|
import { WidgetType } from "../../../widgets/WidgetType";
|
||||||
|
import { WidgetMessagingStore } from "../../../stores/widgets/WidgetMessagingStore";
|
||||||
|
import WidgetUtils from "../../../utils/WidgetUtils";
|
||||||
|
import { ElementWidgetActions } from "../../../stores/widgets/ElementWidgetActions";
|
||||||
|
import { Alignment } from "../elements/Tooltip";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
widgetId: string;
|
||||||
|
room: Room;
|
||||||
|
viewingRoom: boolean;
|
||||||
|
onStartMoving: (e: React.MouseEvent<Element, MouseEvent>) => void;
|
||||||
|
movePersistedElement: MutableRefObject<(() => void) | undefined>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A picture-in-picture view for a widget. Additional controls are shown if the
|
||||||
|
* widget is a call of some sort.
|
||||||
|
*/
|
||||||
|
export const WidgetPip: FC<Props> = ({ widgetId, room, viewingRoom, onStartMoving, movePersistedElement }) => {
|
||||||
|
const widget = useMemo(
|
||||||
|
() => WidgetStore.instance.getApps(room.roomId).find((app) => app.id === widgetId)!,
|
||||||
|
[room, widgetId],
|
||||||
|
);
|
||||||
|
|
||||||
|
const roomName = useTypedEventEmitterState(
|
||||||
|
room,
|
||||||
|
RoomEvent.Name,
|
||||||
|
useCallback(() => room.name, [room]),
|
||||||
|
);
|
||||||
|
|
||||||
|
const call = useCallForWidget(widgetId, room.roomId);
|
||||||
|
|
||||||
|
const onBackClick = useCallback(
|
||||||
|
(ev) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
ev.stopPropagation();
|
||||||
|
|
||||||
|
if (call !== null) {
|
||||||
|
defaultDispatcher.dispatch<ViewRoomPayload>({
|
||||||
|
action: Action.ViewRoom,
|
||||||
|
room_id: room.roomId,
|
||||||
|
view_call: true,
|
||||||
|
metricsTrigger: "WebFloatingCallWindow",
|
||||||
|
});
|
||||||
|
} else if (viewingRoom) {
|
||||||
|
WidgetLayoutStore.instance.moveToContainer(room, widget, Container.Center);
|
||||||
|
} else {
|
||||||
|
defaultDispatcher.dispatch<ViewRoomPayload>({
|
||||||
|
action: Action.ViewRoom,
|
||||||
|
room_id: room.roomId,
|
||||||
|
metricsTrigger: "WebFloatingCallWindow",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[room, call, widget, viewingRoom],
|
||||||
|
);
|
||||||
|
|
||||||
|
const onLeaveClick = useCallback(
|
||||||
|
(ev) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
ev.stopPropagation();
|
||||||
|
|
||||||
|
if (call !== null) {
|
||||||
|
call.disconnect().catch((e) => console.error("Failed to leave call", e));
|
||||||
|
} else {
|
||||||
|
// Assumed to be a Jitsi widget
|
||||||
|
WidgetMessagingStore.instance
|
||||||
|
.getMessagingForUid(WidgetUtils.getWidgetUid(widget))
|
||||||
|
?.transport.send(ElementWidgetActions.HangupCall, {})
|
||||||
|
.catch((e) => console.error("Failed to leave Jitsi", e));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[call, widget],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx_WidgetPip" onMouseDown={onStartMoving} onClick={onBackClick}>
|
||||||
|
<Toolbar className="mx_WidgetPip_header">
|
||||||
|
<RovingAccessibleButton
|
||||||
|
onClick={onBackClick}
|
||||||
|
className="mx_WidgetPip_backButton"
|
||||||
|
aria-label={_t("Back")}
|
||||||
|
>
|
||||||
|
<BackIcon className="mx_Icon mx_Icon_16" />
|
||||||
|
{roomName}
|
||||||
|
</RovingAccessibleButton>
|
||||||
|
</Toolbar>
|
||||||
|
<PersistentApp
|
||||||
|
persistentWidgetId={widgetId}
|
||||||
|
persistentRoomId={room.roomId}
|
||||||
|
pointerEvents="none"
|
||||||
|
movePersistedElement={movePersistedElement}
|
||||||
|
/>
|
||||||
|
{(call !== null || WidgetType.JITSI.matches(widget.type)) && (
|
||||||
|
<Toolbar className="mx_WidgetPip_footer">
|
||||||
|
<RovingAccessibleTooltipButton
|
||||||
|
onClick={onLeaveClick}
|
||||||
|
tooltip={_t("Leave")}
|
||||||
|
aria-label={_t("Leave")}
|
||||||
|
alignment={Alignment.Top}
|
||||||
|
>
|
||||||
|
<HangupIcon className="mx_Icon mx_Icon_24" />
|
||||||
|
</RovingAccessibleTooltipButton>
|
||||||
|
</Toolbar>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -29,8 +29,6 @@ import { CryptoEvent } from "matrix-js-sdk/src/crypto";
|
|||||||
import { UserTrustLevel } from "matrix-js-sdk/src/crypto/CrossSigning";
|
import { UserTrustLevel } from "matrix-js-sdk/src/crypto/CrossSigning";
|
||||||
import { Feature, ServerSupport } from "matrix-js-sdk/src/feature";
|
import { Feature, ServerSupport } from "matrix-js-sdk/src/feature";
|
||||||
|
|
||||||
import { Icon as LinkIcon } from "../../../../res/img/element-icons/link.svg";
|
|
||||||
import { Icon as ViewInRoomIcon } from "../../../../res/img/element-icons/view-in-room.svg";
|
|
||||||
import ReplyChain from "../elements/ReplyChain";
|
import ReplyChain from "../elements/ReplyChain";
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
import dis from "../../../dispatcher/dispatcher";
|
import dis from "../../../dispatcher/dispatcher";
|
||||||
@ -63,8 +61,6 @@ import SettingsStore from "../../../settings/SettingsStore";
|
|||||||
import { MessagePreviewStore } from "../../../stores/room-list/MessagePreviewStore";
|
import { MessagePreviewStore } from "../../../stores/room-list/MessagePreviewStore";
|
||||||
import RoomContext, { TimelineRenderingType } from "../../../contexts/RoomContext";
|
import RoomContext, { TimelineRenderingType } from "../../../contexts/RoomContext";
|
||||||
import { MediaEventHelper } from "../../../utils/MediaEventHelper";
|
import { MediaEventHelper } from "../../../utils/MediaEventHelper";
|
||||||
import Toolbar from "../../../accessibility/Toolbar";
|
|
||||||
import { RovingAccessibleTooltipButton } from "../../../accessibility/roving/RovingAccessibleTooltipButton";
|
|
||||||
import { ThreadNotificationState } from "../../../stores/notifications/ThreadNotificationState";
|
import { ThreadNotificationState } from "../../../stores/notifications/ThreadNotificationState";
|
||||||
import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore";
|
import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore";
|
||||||
import { NotificationStateEvents } from "../../../stores/notifications/NotificationState";
|
import { NotificationStateEvents } from "../../../stores/notifications/NotificationState";
|
||||||
@ -85,6 +81,7 @@ import { ShowThreadPayload } from "../../../dispatcher/payloads/ShowThreadPayloa
|
|||||||
import { isLocalRoom } from "../../../utils/localRoom/isLocalRoom";
|
import { isLocalRoom } from "../../../utils/localRoom/isLocalRoom";
|
||||||
import { ElementCall } from "../../../models/Call";
|
import { ElementCall } from "../../../models/Call";
|
||||||
import { UnreadNotificationBadge } from "./NotificationBadge/UnreadNotificationBadge";
|
import { UnreadNotificationBadge } from "./NotificationBadge/UnreadNotificationBadge";
|
||||||
|
import { EventTileThreadToolbar } from "./EventTile/EventTileThreadToolbar";
|
||||||
|
|
||||||
export type GetRelationsForEvent = (
|
export type GetRelationsForEvent = (
|
||||||
eventId: string,
|
eventId: string,
|
||||||
@ -325,7 +322,12 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
|||||||
// events and pretty much anything that can't be sent by the composer as a message. For
|
// events and pretty much anything that can't be sent by the composer as a message. For
|
||||||
// those we rely on local echo giving the impression of things changing, and expect them
|
// those we rely on local echo giving the impression of things changing, and expect them
|
||||||
// to be quick.
|
// to be quick.
|
||||||
const simpleSendableEvents = [EventType.Sticker, EventType.RoomMessage, EventType.RoomMessageEncrypted];
|
const simpleSendableEvents = [
|
||||||
|
EventType.Sticker,
|
||||||
|
EventType.RoomMessage,
|
||||||
|
EventType.RoomMessageEncrypted,
|
||||||
|
EventType.PollStart,
|
||||||
|
];
|
||||||
if (!simpleSendableEvents.includes(this.props.mxEvent.getType() as EventType)) return false;
|
if (!simpleSendableEvents.includes(this.props.mxEvent.getType() as EventType)) return false;
|
||||||
|
|
||||||
// Default case
|
// Default case
|
||||||
@ -972,6 +974,8 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
|||||||
isContinuation = false;
|
isContinuation = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isRenderingNotification = this.context.timelineRenderingType === TimelineRenderingType.Notification;
|
||||||
|
|
||||||
const isEditing = !!this.props.editState;
|
const isEditing = !!this.props.editState;
|
||||||
const classes = classNames({
|
const classes = classNames({
|
||||||
mx_EventTile_bubbleContainer: isBubbleMessage,
|
mx_EventTile_bubbleContainer: isBubbleMessage,
|
||||||
@ -996,7 +1000,8 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
|||||||
mx_EventTile_bad: isEncryptionFailure,
|
mx_EventTile_bad: isEncryptionFailure,
|
||||||
mx_EventTile_emote: msgtype === MsgType.Emote,
|
mx_EventTile_emote: msgtype === MsgType.Emote,
|
||||||
mx_EventTile_noSender: this.props.hideSender,
|
mx_EventTile_noSender: this.props.hideSender,
|
||||||
mx_EventTile_clamp: this.context.timelineRenderingType === TimelineRenderingType.ThreadsList,
|
mx_EventTile_clamp:
|
||||||
|
this.context.timelineRenderingType === TimelineRenderingType.ThreadsList || isRenderingNotification,
|
||||||
mx_EventTile_noBubble: noBubbleEvent,
|
mx_EventTile_noBubble: noBubbleEvent,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -1012,12 +1017,12 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
|||||||
// Local echos have a send "status".
|
// Local echos have a send "status".
|
||||||
const scrollToken = this.props.mxEvent.status ? undefined : this.props.mxEvent.getId();
|
const scrollToken = this.props.mxEvent.status ? undefined : this.props.mxEvent.getId();
|
||||||
|
|
||||||
let avatar: JSX.Element;
|
let avatar: JSX.Element | null = null;
|
||||||
let sender: JSX.Element;
|
let sender: JSX.Element | null = null;
|
||||||
let avatarSize: number;
|
let avatarSize: number;
|
||||||
let needsSenderProfile: boolean;
|
let needsSenderProfile: boolean;
|
||||||
|
|
||||||
if (this.context.timelineRenderingType === TimelineRenderingType.Notification) {
|
if (isRenderingNotification) {
|
||||||
avatarSize = 24;
|
avatarSize = 24;
|
||||||
needsSenderProfile = true;
|
needsSenderProfile = true;
|
||||||
} else if (isInfoMessage) {
|
} else if (isInfoMessage) {
|
||||||
@ -1061,7 +1066,9 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
|||||||
member = this.props.mxEvent.sender;
|
member = this.props.mxEvent.sender;
|
||||||
}
|
}
|
||||||
// In the ThreadsList view we use the entire EventTile as a click target to open the thread instead
|
// In the ThreadsList view we use the entire EventTile as a click target to open the thread instead
|
||||||
const viewUserOnClick = this.context.timelineRenderingType !== TimelineRenderingType.ThreadsList;
|
const viewUserOnClick = ![TimelineRenderingType.ThreadsList, TimelineRenderingType.Notification].includes(
|
||||||
|
this.context.timelineRenderingType,
|
||||||
|
);
|
||||||
avatar = (
|
avatar = (
|
||||||
<div className="mx_EventTile_avatar">
|
<div className="mx_EventTile_avatar">
|
||||||
<MemberAvatar
|
<MemberAvatar
|
||||||
@ -1202,57 +1209,6 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
|||||||
const isOwnEvent = this.props.mxEvent?.getSender() === MatrixClientPeg.get().getUserId();
|
const isOwnEvent = this.props.mxEvent?.getSender() === MatrixClientPeg.get().getUserId();
|
||||||
|
|
||||||
switch (this.context.timelineRenderingType) {
|
switch (this.context.timelineRenderingType) {
|
||||||
case TimelineRenderingType.Notification: {
|
|
||||||
const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId());
|
|
||||||
return React.createElement(
|
|
||||||
this.props.as || "li",
|
|
||||||
{
|
|
||||||
"className": classes,
|
|
||||||
"aria-live": ariaLive,
|
|
||||||
"aria-atomic": true,
|
|
||||||
"data-scroll-tokens": scrollToken,
|
|
||||||
},
|
|
||||||
[
|
|
||||||
<div className="mx_EventTile_roomName" key="mx_EventTile_roomName">
|
|
||||||
<RoomAvatar room={room} width={28} height={28} />
|
|
||||||
<a href={permalink} onClick={this.onPermalinkClicked}>
|
|
||||||
{room ? room.name : ""}
|
|
||||||
</a>
|
|
||||||
</div>,
|
|
||||||
<div className="mx_EventTile_senderDetails" key="mx_EventTile_senderDetails">
|
|
||||||
{avatar}
|
|
||||||
<a
|
|
||||||
href={permalink}
|
|
||||||
onClick={this.onPermalinkClicked}
|
|
||||||
onContextMenu={this.onTimestampContextMenu}
|
|
||||||
>
|
|
||||||
{sender}
|
|
||||||
{timestamp}
|
|
||||||
</a>
|
|
||||||
</div>,
|
|
||||||
<div className={lineClasses} key="mx_EventTile_line" onContextMenu={this.onContextMenu}>
|
|
||||||
{this.renderContextMenu()}
|
|
||||||
{renderTile(
|
|
||||||
TimelineRenderingType.Notification,
|
|
||||||
{
|
|
||||||
...this.props,
|
|
||||||
|
|
||||||
// overrides
|
|
||||||
ref: this.tile,
|
|
||||||
isSeeingThroughMessageHiddenForModeration,
|
|
||||||
|
|
||||||
// appease TS
|
|
||||||
highlights: this.props.highlights,
|
|
||||||
highlightLink: this.props.highlightLink,
|
|
||||||
onHeightChanged: this.props.onHeightChanged,
|
|
||||||
permalinkCreator: this.props.permalinkCreator,
|
|
||||||
},
|
|
||||||
this.context.showHiddenEvents,
|
|
||||||
)}
|
|
||||||
</div>,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
case TimelineRenderingType.Thread: {
|
case TimelineRenderingType.Thread: {
|
||||||
return React.createElement(
|
return React.createElement(
|
||||||
this.props.as || "li",
|
this.props.as || "li",
|
||||||
@ -1289,8 +1245,8 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
|||||||
// appease TS
|
// appease TS
|
||||||
highlights: this.props.highlights,
|
highlights: this.props.highlights,
|
||||||
highlightLink: this.props.highlightLink,
|
highlightLink: this.props.highlightLink,
|
||||||
onHeightChanged: this.props.onHeightChanged,
|
onHeightChanged: () => this.props.onHeightChanged,
|
||||||
permalinkCreator: this.props.permalinkCreator,
|
permalinkCreator: this.props.permalinkCreator!,
|
||||||
},
|
},
|
||||||
this.context.showHiddenEvents,
|
this.context.showHiddenEvents,
|
||||||
)}
|
)}
|
||||||
@ -1304,6 +1260,7 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
case TimelineRenderingType.Notification:
|
||||||
case TimelineRenderingType.ThreadsList: {
|
case TimelineRenderingType.ThreadsList: {
|
||||||
const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId());
|
const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId());
|
||||||
// tab-index=-1 to allow it to be focusable but do not add tab stop for it, primarily for screen readers
|
// tab-index=-1 to allow it to be focusable but do not add tab stop for it, primarily for screen readers
|
||||||
@ -1326,20 +1283,48 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
|||||||
"onMouseEnter": () => this.setState({ hover: true }),
|
"onMouseEnter": () => this.setState({ hover: true }),
|
||||||
"onMouseLeave": () => this.setState({ hover: false }),
|
"onMouseLeave": () => this.setState({ hover: false }),
|
||||||
"onClick": (ev: MouseEvent) => {
|
"onClick": (ev: MouseEvent) => {
|
||||||
dis.dispatch<ShowThreadPayload>({
|
|
||||||
action: Action.ShowThread,
|
|
||||||
rootEvent: this.props.mxEvent,
|
|
||||||
push: true,
|
|
||||||
});
|
|
||||||
const target = ev.currentTarget as HTMLElement;
|
const target = ev.currentTarget as HTMLElement;
|
||||||
const index = Array.from(target.parentElement.children).indexOf(target);
|
let index = -1;
|
||||||
PosthogTrackers.trackInteraction("WebThreadsPanelThreadItem", ev, index);
|
if (target.parentElement) index = Array.from(target.parentElement.children).indexOf(target);
|
||||||
|
switch (this.context.timelineRenderingType) {
|
||||||
|
case TimelineRenderingType.Notification:
|
||||||
|
this.viewInRoom(ev);
|
||||||
|
break;
|
||||||
|
case TimelineRenderingType.ThreadsList:
|
||||||
|
dis.dispatch<ShowThreadPayload>({
|
||||||
|
action: Action.ShowThread,
|
||||||
|
rootEvent: this.props.mxEvent,
|
||||||
|
push: true,
|
||||||
|
});
|
||||||
|
PosthogTrackers.trackInteraction("WebThreadsPanelThreadItem", ev, index ?? -1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
<>
|
<>
|
||||||
{sender}
|
<div className="mx_EventTile_details">
|
||||||
{avatar}
|
{sender}
|
||||||
{timestamp}
|
{isRenderingNotification && room ? (
|
||||||
|
<span className="mx_EventTile_truncated">
|
||||||
|
{" "}
|
||||||
|
{_t(
|
||||||
|
" in <strong>%(room)s</strong>",
|
||||||
|
{ room: room.name },
|
||||||
|
{ strong: (sub) => <strong>{sub}</strong> },
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
""
|
||||||
|
)}
|
||||||
|
{timestamp}
|
||||||
|
</div>
|
||||||
|
{isRenderingNotification && room ? (
|
||||||
|
<div className="mx_EventTile_avatar">
|
||||||
|
<RoomAvatar room={room} width={28} height={28} />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
avatar
|
||||||
|
)}
|
||||||
<div className={lineClasses} key="mx_EventTile_line">
|
<div className={lineClasses} key="mx_EventTile_line">
|
||||||
<div className="mx_EventTile_body">
|
<div className="mx_EventTile_body">
|
||||||
{this.props.mxEvent.isRedacted() ? (
|
{this.props.mxEvent.isRedacted() ? (
|
||||||
@ -1350,24 +1335,13 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
|||||||
</div>
|
</div>
|
||||||
{this.renderThreadPanelSummary()}
|
{this.renderThreadPanelSummary()}
|
||||||
</div>
|
</div>
|
||||||
<Toolbar className="mx_MessageActionBar" aria-label={_t("Message Actions")} aria-live="off">
|
{this.context.timelineRenderingType === TimelineRenderingType.ThreadsList && (
|
||||||
<RovingAccessibleTooltipButton
|
<EventTileThreadToolbar
|
||||||
className="mx_MessageActionBar_iconButton"
|
viewInRoom={this.viewInRoom}
|
||||||
onClick={this.viewInRoom}
|
copyLinkToThread={this.copyLinkToThread}
|
||||||
title={_t("View in room")}
|
/>
|
||||||
key="view_in_room"
|
)}
|
||||||
>
|
|
||||||
<ViewInRoomIcon />
|
|
||||||
</RovingAccessibleTooltipButton>
|
|
||||||
<RovingAccessibleTooltipButton
|
|
||||||
className="mx_MessageActionBar_iconButton"
|
|
||||||
onClick={this.copyLinkToThread}
|
|
||||||
title={_t("Copy link to thread")}
|
|
||||||
key="copy_link_to_thread"
|
|
||||||
>
|
|
||||||
<LinkIcon />
|
|
||||||
</RovingAccessibleTooltipButton>
|
|
||||||
</Toolbar>
|
|
||||||
{msgOption}
|
{msgOption}
|
||||||
<UnreadNotificationBadge room={room} threadId={this.props.mxEvent.getId()} />
|
<UnreadNotificationBadge room={room} threadId={this.props.mxEvent.getId()} />
|
||||||
</>,
|
</>,
|
||||||
|
@ -0,0 +1,53 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { RovingAccessibleTooltipButton } from "../../../../accessibility/RovingTabIndex";
|
||||||
|
import Toolbar from "../../../../accessibility/Toolbar";
|
||||||
|
import { _t } from "../../../../languageHandler";
|
||||||
|
import { Icon as LinkIcon } from "../../../../../res/img/element-icons/link.svg";
|
||||||
|
import { Icon as ViewInRoomIcon } from "../../../../../res/img/element-icons/view-in-room.svg";
|
||||||
|
import { ButtonEvent } from "../../elements/AccessibleButton";
|
||||||
|
|
||||||
|
export function EventTileThreadToolbar({
|
||||||
|
viewInRoom,
|
||||||
|
copyLinkToThread,
|
||||||
|
}: {
|
||||||
|
viewInRoom: (evt: ButtonEvent) => void;
|
||||||
|
copyLinkToThread: (evt: ButtonEvent) => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Toolbar className="mx_MessageActionBar" aria-label={_t("Message Actions")} aria-live="off">
|
||||||
|
<RovingAccessibleTooltipButton
|
||||||
|
className="mx_MessageActionBar_iconButton"
|
||||||
|
onClick={viewInRoom}
|
||||||
|
title={_t("View in room")}
|
||||||
|
key="view_in_room"
|
||||||
|
>
|
||||||
|
<ViewInRoomIcon />
|
||||||
|
</RovingAccessibleTooltipButton>
|
||||||
|
<RovingAccessibleTooltipButton
|
||||||
|
className="mx_MessageActionBar_iconButton"
|
||||||
|
onClick={copyLinkToThread}
|
||||||
|
title={_t("Copy link to thread")}
|
||||||
|
key="copy_link_to_thread"
|
||||||
|
>
|
||||||
|
<LinkIcon />
|
||||||
|
</RovingAccessibleTooltipButton>
|
||||||
|
</Toolbar>
|
||||||
|
);
|
||||||
|
}
|
@ -54,10 +54,8 @@ import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
|
|||||||
import { isLocalRoom } from "../../../utils/localRoom/isLocalRoom";
|
import { isLocalRoom } from "../../../utils/localRoom/isLocalRoom";
|
||||||
import { Features } from "../../../settings/Settings";
|
import { Features } from "../../../settings/Settings";
|
||||||
import { VoiceMessageRecording } from "../../../audio/VoiceMessageRecording";
|
import { VoiceMessageRecording } from "../../../audio/VoiceMessageRecording";
|
||||||
import { VoiceBroadcastRecordingsStore } from "../../../voice-broadcast";
|
import { SendWysiwygComposer, sendMessage, getConversionFunctions } from "./wysiwyg_composer/";
|
||||||
import { SendWysiwygComposer, sendMessage } from "./wysiwyg_composer/";
|
|
||||||
import { MatrixClientProps, withMatrixClientHOC } from "../../../contexts/MatrixClientContext";
|
import { MatrixClientProps, withMatrixClientHOC } from "../../../contexts/MatrixClientContext";
|
||||||
import { htmlToPlainText } from "../../../utils/room/htmlToPlaintext";
|
|
||||||
import { setUpVoiceBroadcastPreRecording } from "../../../voice-broadcast/utils/setUpVoiceBroadcastPreRecording";
|
import { setUpVoiceBroadcastPreRecording } from "../../../voice-broadcast/utils/setUpVoiceBroadcastPreRecording";
|
||||||
import { SdkContextClass } from "../../../contexts/SDKContext";
|
import { SdkContextClass } from "../../../contexts/SDKContext";
|
||||||
|
|
||||||
@ -334,7 +332,7 @@ export class MessageComposer extends React.Component<IProps, IState> {
|
|||||||
|
|
||||||
if (this.state.isWysiwygLabEnabled) {
|
if (this.state.isWysiwygLabEnabled) {
|
||||||
const { permalinkCreator, relation, replyToEvent } = this.props;
|
const { permalinkCreator, relation, replyToEvent } = this.props;
|
||||||
sendMessage(this.state.composerContent, this.state.isRichTextEnabled, {
|
await sendMessage(this.state.composerContent, this.state.isRichTextEnabled, {
|
||||||
mxClient: this.props.mxClient,
|
mxClient: this.props.mxClient,
|
||||||
roomContext: this.context,
|
roomContext: this.context,
|
||||||
permalinkCreator,
|
permalinkCreator,
|
||||||
@ -359,14 +357,19 @@ export class MessageComposer extends React.Component<IProps, IState> {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
private onRichTextToggle = () => {
|
private onRichTextToggle = async () => {
|
||||||
this.setState((state) => ({
|
const { richToPlain, plainToRich } = await getConversionFunctions();
|
||||||
isRichTextEnabled: !state.isRichTextEnabled,
|
|
||||||
initialComposerContent: !state.isRichTextEnabled
|
const { isRichTextEnabled, composerContent } = this.state;
|
||||||
? state.composerContent
|
const convertedContent = isRichTextEnabled
|
||||||
: // TODO when available use rust model plain text
|
? await richToPlain(composerContent)
|
||||||
htmlToPlainText(state.composerContent),
|
: await plainToRich(composerContent);
|
||||||
}));
|
|
||||||
|
this.setState({
|
||||||
|
isRichTextEnabled: !isRichTextEnabled,
|
||||||
|
composerContent: convertedContent,
|
||||||
|
initialComposerContent: convertedContent,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
private onVoiceStoreUpdate = () => {
|
private onVoiceStoreUpdate = () => {
|
||||||
@ -604,7 +607,7 @@ export class MessageComposer extends React.Component<IProps, IState> {
|
|||||||
this.props.room,
|
this.props.room,
|
||||||
MatrixClientPeg.get(),
|
MatrixClientPeg.get(),
|
||||||
SdkContextClass.instance.voiceBroadcastPlaybacksStore,
|
SdkContextClass.instance.voiceBroadcastPlaybacksStore,
|
||||||
VoiceBroadcastRecordingsStore.instance(),
|
SdkContextClass.instance.voiceBroadcastRecordingsStore,
|
||||||
SdkContextClass.instance.voiceBroadcastPreRecordingStore,
|
SdkContextClass.instance.voiceBroadcastPreRecordingStore,
|
||||||
);
|
);
|
||||||
this.toggleButtonMenu();
|
this.toggleButtonMenu();
|
||||||
|
@ -16,7 +16,6 @@ limitations under the License.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { SearchResult } from "matrix-js-sdk/src/models/search-result";
|
|
||||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||||
|
|
||||||
import RoomContext, { TimelineRenderingType } from "../../../contexts/RoomContext";
|
import RoomContext, { TimelineRenderingType } from "../../../contexts/RoomContext";
|
||||||
@ -30,12 +29,14 @@ import LegacyCallEventGrouper, { buildLegacyCallEventGroupers } from "../../stru
|
|||||||
import { haveRendererForEvent } from "../../../events/EventTileFactory";
|
import { haveRendererForEvent } from "../../../events/EventTileFactory";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
// a matrix-js-sdk SearchResult containing the details of this result
|
|
||||||
searchResult: SearchResult;
|
|
||||||
// a list of strings to be highlighted in the results
|
// a list of strings to be highlighted in the results
|
||||||
searchHighlights?: string[];
|
searchHighlights?: string[];
|
||||||
// href for the highlights in this result
|
// href for the highlights in this result
|
||||||
resultLink?: string;
|
resultLink?: string;
|
||||||
|
// timeline of the search result
|
||||||
|
timeline: MatrixEvent[];
|
||||||
|
// indexes of the matching events (not contextual ones)
|
||||||
|
ourEventsIndexes: number[];
|
||||||
onHeightChanged?: () => void;
|
onHeightChanged?: () => void;
|
||||||
permalinkCreator?: RoomPermalinkCreator;
|
permalinkCreator?: RoomPermalinkCreator;
|
||||||
}
|
}
|
||||||
@ -50,7 +51,7 @@ export default class SearchResultTile extends React.Component<IProps> {
|
|||||||
public constructor(props, context) {
|
public constructor(props, context) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
|
|
||||||
this.buildLegacyCallEventGroupers(this.props.searchResult.context.getTimeline());
|
this.buildLegacyCallEventGroupers(this.props.timeline);
|
||||||
}
|
}
|
||||||
|
|
||||||
private buildLegacyCallEventGroupers(events?: MatrixEvent[]): void {
|
private buildLegacyCallEventGroupers(events?: MatrixEvent[]): void {
|
||||||
@ -58,8 +59,8 @@ export default class SearchResultTile extends React.Component<IProps> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
const result = this.props.searchResult;
|
const timeline = this.props.timeline;
|
||||||
const resultEvent = result.context.getEvent();
|
const resultEvent = timeline[this.props.ourEventsIndexes[0]];
|
||||||
const eventId = resultEvent.getId();
|
const eventId = resultEvent.getId();
|
||||||
|
|
||||||
const ts1 = resultEvent.getTs();
|
const ts1 = resultEvent.getTs();
|
||||||
@ -69,11 +70,10 @@ export default class SearchResultTile extends React.Component<IProps> {
|
|||||||
const alwaysShowTimestamps = SettingsStore.getValue("alwaysShowTimestamps");
|
const alwaysShowTimestamps = SettingsStore.getValue("alwaysShowTimestamps");
|
||||||
const threadsEnabled = SettingsStore.getValue("feature_threadstable");
|
const threadsEnabled = SettingsStore.getValue("feature_threadstable");
|
||||||
|
|
||||||
const timeline = result.context.getTimeline();
|
|
||||||
for (let j = 0; j < timeline.length; j++) {
|
for (let j = 0; j < timeline.length; j++) {
|
||||||
const mxEv = timeline[j];
|
const mxEv = timeline[j];
|
||||||
let highlights;
|
let highlights;
|
||||||
const contextual = j != result.context.getOurEventIndex();
|
const contextual = !this.props.ourEventsIndexes.includes(j);
|
||||||
if (!contextual) {
|
if (!contextual) {
|
||||||
highlights = this.props.searchHighlights;
|
highlights = this.props.searchHighlights;
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,36 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { createContext, useContext } from "react";
|
||||||
|
|
||||||
|
import { SubSelection } from "./types";
|
||||||
|
|
||||||
|
export function getDefaultContextValue(): { selection: SubSelection } {
|
||||||
|
return {
|
||||||
|
selection: { anchorNode: null, anchorOffset: 0, focusNode: null, focusOffset: 0 },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ComposerContextState {
|
||||||
|
selection: SubSelection;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ComposerContext = createContext<ComposerContextState>(getDefaultContextValue());
|
||||||
|
ComposerContext.displayName = "ComposerContext";
|
||||||
|
|
||||||
|
export function useComposerContext() {
|
||||||
|
return useContext(ComposerContext);
|
||||||
|
}
|
@ -0,0 +1,52 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { ComponentProps, lazy, Suspense } from "react";
|
||||||
|
|
||||||
|
// we need to import the types for TS, but do not import the sendMessage
|
||||||
|
// function to avoid importing from "@matrix-org/matrix-wysiwyg"
|
||||||
|
import { SendMessageParams } from "./utils/message";
|
||||||
|
|
||||||
|
const SendComposer = lazy(() => import("./SendWysiwygComposer"));
|
||||||
|
const EditComposer = lazy(() => import("./EditWysiwygComposer"));
|
||||||
|
|
||||||
|
export const dynamicImportSendMessage = async (message: string, isHTML: boolean, params: SendMessageParams) => {
|
||||||
|
const { sendMessage } = await import("./utils/message");
|
||||||
|
|
||||||
|
return sendMessage(message, isHTML, params);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const dynamicImportConversionFunctions = async () => {
|
||||||
|
const { richToPlain, plainToRich } = await import("@matrix-org/matrix-wysiwyg");
|
||||||
|
|
||||||
|
return { richToPlain, plainToRich };
|
||||||
|
};
|
||||||
|
|
||||||
|
export function DynamicImportSendWysiwygComposer(props: ComponentProps<typeof SendComposer>) {
|
||||||
|
return (
|
||||||
|
<Suspense fallback={<div />}>
|
||||||
|
<SendComposer {...props} />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DynamicImportEditWysiwygComposer(props: ComponentProps<typeof EditComposer>) {
|
||||||
|
return (
|
||||||
|
<Suspense fallback={<div />}>
|
||||||
|
<EditComposer {...props} />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { forwardRef, RefObject } from "react";
|
import React, { ForwardedRef, forwardRef, MutableRefObject, useRef } from "react";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
|
||||||
import EditorStateTransfer from "../../../../utils/EditorStateTransfer";
|
import EditorStateTransfer from "../../../../utils/EditorStateTransfer";
|
||||||
@ -23,16 +23,19 @@ import { EditionButtons } from "./components/EditionButtons";
|
|||||||
import { useWysiwygEditActionHandler } from "./hooks/useWysiwygEditActionHandler";
|
import { useWysiwygEditActionHandler } from "./hooks/useWysiwygEditActionHandler";
|
||||||
import { useEditing } from "./hooks/useEditing";
|
import { useEditing } from "./hooks/useEditing";
|
||||||
import { useInitialContent } from "./hooks/useInitialContent";
|
import { useInitialContent } from "./hooks/useInitialContent";
|
||||||
|
import { ComposerContext, getDefaultContextValue } from "./ComposerContext";
|
||||||
|
import { ComposerFunctions } from "./types";
|
||||||
|
|
||||||
interface ContentProps {
|
interface ContentProps {
|
||||||
disabled: boolean;
|
disabled?: boolean;
|
||||||
|
composerFunctions: ComposerFunctions;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Content = forwardRef<HTMLElement, ContentProps>(function Content(
|
const Content = forwardRef<HTMLElement, ContentProps>(function Content(
|
||||||
{ disabled }: ContentProps,
|
{ disabled = false, composerFunctions }: ContentProps,
|
||||||
forwardRef: RefObject<HTMLElement>,
|
forwardRef: ForwardedRef<HTMLElement>,
|
||||||
) {
|
) {
|
||||||
useWysiwygEditActionHandler(disabled, forwardRef);
|
useWysiwygEditActionHandler(disabled, forwardRef as MutableRefObject<HTMLElement>, composerFunctions);
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -43,14 +46,20 @@ interface EditWysiwygComposerProps {
|
|||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function EditWysiwygComposer({ editorStateTransfer, className, ...props }: EditWysiwygComposerProps) {
|
// Default needed for React.lazy
|
||||||
|
export default function EditWysiwygComposer({ editorStateTransfer, className, ...props }: EditWysiwygComposerProps) {
|
||||||
|
const defaultContextValue = useRef(getDefaultContextValue());
|
||||||
const initialContent = useInitialContent(editorStateTransfer);
|
const initialContent = useInitialContent(editorStateTransfer);
|
||||||
const isReady = !editorStateTransfer || initialContent !== undefined;
|
const isReady = !editorStateTransfer || initialContent !== undefined;
|
||||||
|
|
||||||
const { editMessage, endEditing, onChange, isSaveDisabled } = useEditing(editorStateTransfer, initialContent);
|
const { editMessage, endEditing, onChange, isSaveDisabled } = useEditing(editorStateTransfer, initialContent);
|
||||||
|
|
||||||
|
if (!isReady) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
isReady && (
|
<ComposerContext.Provider value={defaultContextValue.current}>
|
||||||
<WysiwygComposer
|
<WysiwygComposer
|
||||||
className={classNames("mx_EditWysiwygComposer", className)}
|
className={classNames("mx_EditWysiwygComposer", className)}
|
||||||
initialContent={initialContent}
|
initialContent={initialContent}
|
||||||
@ -58,9 +67,9 @@ export function EditWysiwygComposer({ editorStateTransfer, className, ...props }
|
|||||||
onSend={editMessage}
|
onSend={editMessage}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{(ref) => (
|
{(ref, composerFunctions) => (
|
||||||
<>
|
<>
|
||||||
<Content disabled={props.disabled} ref={ref} />
|
<Content disabled={props.disabled} ref={ref} composerFunctions={composerFunctions} />
|
||||||
<EditionButtons
|
<EditionButtons
|
||||||
onCancelClick={endEditing}
|
onCancelClick={endEditing}
|
||||||
onSaveClick={editMessage}
|
onSaveClick={editMessage}
|
||||||
@ -69,6 +78,6 @@ export function EditWysiwygComposer({ editorStateTransfer, className, ...props }
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</WysiwygComposer>
|
</WysiwygComposer>
|
||||||
)
|
</ComposerContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { ForwardedRef, forwardRef, MutableRefObject } from "react";
|
import React, { ForwardedRef, forwardRef, MutableRefObject, useRef } from "react";
|
||||||
|
|
||||||
import { useWysiwygSendActionHandler } from "./hooks/useWysiwygSendActionHandler";
|
import { useWysiwygSendActionHandler } from "./hooks/useWysiwygSendActionHandler";
|
||||||
import { WysiwygComposer } from "./components/WysiwygComposer";
|
import { WysiwygComposer } from "./components/WysiwygComposer";
|
||||||
@ -24,6 +24,7 @@ import { E2EStatus } from "../../../../utils/ShieldUtils";
|
|||||||
import E2EIcon from "../E2EIcon";
|
import E2EIcon from "../E2EIcon";
|
||||||
import { AboveLeftOf } from "../../../structures/ContextMenu";
|
import { AboveLeftOf } from "../../../structures/ContextMenu";
|
||||||
import { Emoji } from "./components/Emoji";
|
import { Emoji } from "./components/Emoji";
|
||||||
|
import { ComposerContext, getDefaultContextValue } from "./ComposerContext";
|
||||||
|
|
||||||
interface ContentProps {
|
interface ContentProps {
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
@ -49,26 +50,28 @@ interface SendWysiwygComposerProps {
|
|||||||
menuPosition: AboveLeftOf;
|
menuPosition: AboveLeftOf;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SendWysiwygComposer({
|
// Default needed for React.lazy
|
||||||
|
export default function SendWysiwygComposer({
|
||||||
isRichTextEnabled,
|
isRichTextEnabled,
|
||||||
e2eStatus,
|
e2eStatus,
|
||||||
menuPosition,
|
menuPosition,
|
||||||
...props
|
...props
|
||||||
}: SendWysiwygComposerProps) {
|
}: SendWysiwygComposerProps) {
|
||||||
const Composer = isRichTextEnabled ? WysiwygComposer : PlainTextComposer;
|
const Composer = isRichTextEnabled ? WysiwygComposer : PlainTextComposer;
|
||||||
|
const defaultContextValue = useRef(getDefaultContextValue());
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Composer
|
<ComposerContext.Provider value={defaultContextValue.current}>
|
||||||
className="mx_SendWysiwygComposer"
|
<Composer
|
||||||
leftComponent={e2eStatus && <E2EIcon status={e2eStatus} />}
|
className="mx_SendWysiwygComposer"
|
||||||
rightComponent={(selectPreviousSelection) => (
|
leftComponent={e2eStatus && <E2EIcon status={e2eStatus} />}
|
||||||
<Emoji menuPosition={menuPosition} selectPreviousSelection={selectPreviousSelection} />
|
rightComponent={<Emoji menuPosition={menuPosition} />}
|
||||||
)}
|
{...props}
|
||||||
{...props}
|
>
|
||||||
>
|
{(ref, composerFunctions) => (
|
||||||
{(ref, composerFunctions) => (
|
<Content disabled={props.disabled} ref={ref} composerFunctions={composerFunctions} />
|
||||||
<Content disabled={props.disabled} ref={ref} composerFunctions={composerFunctions} />
|
)}
|
||||||
)}
|
</Composer>
|
||||||
</Composer>
|
</ComposerContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -26,7 +26,7 @@ interface EditorProps {
|
|||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
leftComponent?: ReactNode;
|
leftComponent?: ReactNode;
|
||||||
rightComponent?: (selectPreviousSelection: () => void) => ReactNode;
|
rightComponent?: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Editor = memo(
|
export const Editor = memo(
|
||||||
@ -35,7 +35,7 @@ export const Editor = memo(
|
|||||||
ref,
|
ref,
|
||||||
) {
|
) {
|
||||||
const isExpanded = useIsExpanded(ref as MutableRefObject<HTMLDivElement | null>, HEIGHT_BREAKING_POINT);
|
const isExpanded = useIsExpanded(ref as MutableRefObject<HTMLDivElement | null>, HEIGHT_BREAKING_POINT);
|
||||||
const { onFocus, onBlur, selectPreviousSelection, onInput } = useSelection();
|
const { onFocus, onBlur, onInput } = useSelection();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@ -63,7 +63,7 @@ export const Editor = memo(
|
|||||||
onInput={onInput}
|
onInput={onInput}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{rightComponent?.(selectPreviousSelection)}
|
{rightComponent}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
|
@ -24,18 +24,16 @@ import { Action } from "../../../../../dispatcher/actions";
|
|||||||
import { useRoomContext } from "../../../../../contexts/RoomContext";
|
import { useRoomContext } from "../../../../../contexts/RoomContext";
|
||||||
|
|
||||||
interface EmojiProps {
|
interface EmojiProps {
|
||||||
selectPreviousSelection: () => void;
|
|
||||||
menuPosition: AboveLeftOf;
|
menuPosition: AboveLeftOf;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Emoji({ selectPreviousSelection, menuPosition }: EmojiProps) {
|
export function Emoji({ menuPosition }: EmojiProps) {
|
||||||
const roomContext = useRoomContext();
|
const roomContext = useRoomContext();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EmojiButton
|
<EmojiButton
|
||||||
menuPosition={menuPosition}
|
menuPosition={menuPosition}
|
||||||
addEmoji={(emoji) => {
|
addEmoji={(emoji) => {
|
||||||
selectPreviousSelection();
|
|
||||||
dis.dispatch<ComposerInsertPayload>({
|
dis.dispatch<ComposerInsertPayload>({
|
||||||
action: Action.ComposerInsert,
|
action: Action.ComposerInsert,
|
||||||
text: emoji,
|
text: emoji,
|
||||||
|
@ -23,12 +23,15 @@ import { Icon as ItalicIcon } from "../../../../../../res/img/element-icons/room
|
|||||||
import { Icon as UnderlineIcon } from "../../../../../../res/img/element-icons/room/composer/underline.svg";
|
import { Icon as UnderlineIcon } from "../../../../../../res/img/element-icons/room/composer/underline.svg";
|
||||||
import { Icon as StrikeThroughIcon } from "../../../../../../res/img/element-icons/room/composer/strikethrough.svg";
|
import { Icon as StrikeThroughIcon } from "../../../../../../res/img/element-icons/room/composer/strikethrough.svg";
|
||||||
import { Icon as InlineCodeIcon } from "../../../../../../res/img/element-icons/room/composer/inline_code.svg";
|
import { Icon as InlineCodeIcon } from "../../../../../../res/img/element-icons/room/composer/inline_code.svg";
|
||||||
|
import { Icon as LinkIcon } from "../../../../../../res/img/element-icons/room/composer/link.svg";
|
||||||
import AccessibleTooltipButton from "../../../elements/AccessibleTooltipButton";
|
import AccessibleTooltipButton from "../../../elements/AccessibleTooltipButton";
|
||||||
import { Alignment } from "../../../elements/Tooltip";
|
import { Alignment } from "../../../elements/Tooltip";
|
||||||
import { KeyboardShortcut } from "../../../settings/KeyboardShortcut";
|
import { KeyboardShortcut } from "../../../settings/KeyboardShortcut";
|
||||||
import { KeyCombo } from "../../../../../KeyBindingsManager";
|
import { KeyCombo } from "../../../../../KeyBindingsManager";
|
||||||
import { _td } from "../../../../../languageHandler";
|
import { _td } from "../../../../../languageHandler";
|
||||||
import { ButtonEvent } from "../../../elements/AccessibleButton";
|
import { ButtonEvent } from "../../../elements/AccessibleButton";
|
||||||
|
import { openLinkModal } from "./LinkModal";
|
||||||
|
import { useComposerContext } from "../ComposerContext";
|
||||||
|
|
||||||
interface TooltipProps {
|
interface TooltipProps {
|
||||||
label: string;
|
label: string;
|
||||||
@ -76,6 +79,8 @@ interface FormattingButtonsProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function FormattingButtons({ composer, actionStates }: FormattingButtonsProps) {
|
export function FormattingButtons({ composer, actionStates }: FormattingButtonsProps) {
|
||||||
|
const composerContext = useComposerContext();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_FormattingButtons">
|
<div className="mx_FormattingButtons">
|
||||||
<Button
|
<Button
|
||||||
@ -112,6 +117,12 @@ export function FormattingButtons({ composer, actionStates }: FormattingButtonsP
|
|||||||
onClick={() => composer.inlineCode()}
|
onClick={() => composer.inlineCode()}
|
||||||
icon={<InlineCodeIcon className="mx_FormattingButtons_Icon" />}
|
icon={<InlineCodeIcon className="mx_FormattingButtons_Icon" />}
|
||||||
/>
|
/>
|
||||||
|
<Button
|
||||||
|
isActive={actionStates.link === "reversed"}
|
||||||
|
label={_td("Link")}
|
||||||
|
onClick={() => openLinkModal(composer, composerContext)}
|
||||||
|
icon={<LinkIcon className="mx_FormattingButtons_Icon" />}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,90 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { FormattingFunctions } from "@matrix-org/matrix-wysiwyg";
|
||||||
|
import React, { ChangeEvent, useState } from "react";
|
||||||
|
|
||||||
|
import { _td } from "../../../../../languageHandler";
|
||||||
|
import Modal from "../../../../../Modal";
|
||||||
|
import QuestionDialog from "../../../dialogs/QuestionDialog";
|
||||||
|
import Field from "../../../elements/Field";
|
||||||
|
import { ComposerContextState } from "../ComposerContext";
|
||||||
|
import { isSelectionEmpty, setSelection } from "../utils/selection";
|
||||||
|
|
||||||
|
export function openLinkModal(composer: FormattingFunctions, composerContext: ComposerContextState) {
|
||||||
|
const modal = Modal.createDialog(
|
||||||
|
LinkModal,
|
||||||
|
{ composerContext, composer, onClose: () => modal.close(), isTextEnabled: isSelectionEmpty() },
|
||||||
|
"mx_CompoundDialog",
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isEmpty(text: string) {
|
||||||
|
return text.length < 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LinkModalProps {
|
||||||
|
composer: FormattingFunctions;
|
||||||
|
isTextEnabled: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
composerContext: ComposerContextState;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LinkModal({ composer, isTextEnabled, onClose, composerContext }: LinkModalProps) {
|
||||||
|
const [fields, setFields] = useState({ text: "", link: "" });
|
||||||
|
const isSaveDisabled = (isTextEnabled && isEmpty(fields.text)) || isEmpty(fields.link);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<QuestionDialog
|
||||||
|
className="mx_LinkModal"
|
||||||
|
title={_td("Create a link")}
|
||||||
|
button={_td("Save")}
|
||||||
|
buttonDisabled={isSaveDisabled}
|
||||||
|
hasCancelButton={true}
|
||||||
|
onFinished={async (isClickOnSave: boolean) => {
|
||||||
|
if (isClickOnSave) {
|
||||||
|
await setSelection(composerContext.selection);
|
||||||
|
composer.link(fields.link, isTextEnabled ? fields.text : undefined);
|
||||||
|
}
|
||||||
|
onClose();
|
||||||
|
}}
|
||||||
|
description={
|
||||||
|
<div className="mx_LinkModal_content">
|
||||||
|
{isTextEnabled && (
|
||||||
|
<Field
|
||||||
|
autoFocus={true}
|
||||||
|
label={_td("Text")}
|
||||||
|
value={fields.text}
|
||||||
|
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
||||||
|
setFields((fields) => ({ ...fields, text: e.target.value }))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Field
|
||||||
|
autoFocus={!isTextEnabled}
|
||||||
|
label={_td("Link")}
|
||||||
|
value={fields.link}
|
||||||
|
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
||||||
|
setFields((fields) => ({ ...fields, link: e.target.value }))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
@ -33,7 +33,7 @@ interface PlainTextComposerProps {
|
|||||||
initialContent?: string;
|
initialContent?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
leftComponent?: ReactNode;
|
leftComponent?: ReactNode;
|
||||||
rightComponent?: (selectPreviousSelection: () => void) => ReactNode;
|
rightComponent?: ReactNode;
|
||||||
children?: (ref: MutableRefObject<HTMLDivElement | null>, composerFunctions: ComposerFunctions) => ReactNode;
|
children?: (ref: MutableRefObject<HTMLDivElement | null>, composerFunctions: ComposerFunctions) => ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -32,7 +32,7 @@ interface WysiwygComposerProps {
|
|||||||
initialContent?: string;
|
initialContent?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
leftComponent?: ReactNode;
|
leftComponent?: ReactNode;
|
||||||
rightComponent?: (selectPreviousSelection: () => void) => ReactNode;
|
rightComponent?: ReactNode;
|
||||||
children?: (ref: MutableRefObject<HTMLDivElement | null>, wysiwyg: FormattingFunctions) => ReactNode;
|
children?: (ref: MutableRefObject<HTMLDivElement | null>, wysiwyg: FormattingFunctions) => ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,11 +17,22 @@ limitations under the License.
|
|||||||
import { KeyboardEvent, SyntheticEvent, useCallback, useRef, useState } from "react";
|
import { KeyboardEvent, SyntheticEvent, useCallback, useRef, useState } from "react";
|
||||||
|
|
||||||
import { useSettingValue } from "../../../../../hooks/useSettings";
|
import { useSettingValue } from "../../../../../hooks/useSettings";
|
||||||
|
import { IS_MAC, Key } from "../../../../../Keyboard";
|
||||||
|
|
||||||
function isDivElement(target: EventTarget): target is HTMLDivElement {
|
function isDivElement(target: EventTarget): target is HTMLDivElement {
|
||||||
return target instanceof HTMLDivElement;
|
return target instanceof HTMLDivElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Hitting enter inside the editor inserts an editable div, initially containing a <br />
|
||||||
|
// For correct display, first replace this pattern with a newline character and then remove divs
|
||||||
|
// noting that they are used to delimit paragraphs
|
||||||
|
function amendInnerHtml(text: string) {
|
||||||
|
return text
|
||||||
|
.replace(/<div><br><\/div>/g, "\n") // this is pressing enter then not typing
|
||||||
|
.replace(/<div>/g, "\n") // this is from pressing enter, then typing inside the div
|
||||||
|
.replace(/<\/div>/g, "");
|
||||||
|
}
|
||||||
|
|
||||||
export function usePlainTextListeners(
|
export function usePlainTextListeners(
|
||||||
initialContent?: string,
|
initialContent?: string,
|
||||||
onChange?: (content: string) => void,
|
onChange?: (content: string) => void,
|
||||||
@ -44,25 +55,39 @@ export function usePlainTextListeners(
|
|||||||
[onChange],
|
[onChange],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const enterShouldSend = !useSettingValue<boolean>("MessageComposerInput.ctrlEnterToSend");
|
||||||
const onInput = useCallback(
|
const onInput = useCallback(
|
||||||
(event: SyntheticEvent<HTMLDivElement, InputEvent | ClipboardEvent>) => {
|
(event: SyntheticEvent<HTMLDivElement, InputEvent | ClipboardEvent>) => {
|
||||||
if (isDivElement(event.target)) {
|
if (isDivElement(event.target)) {
|
||||||
setText(event.target.innerHTML);
|
// if enterShouldSend, we do not need to amend the html before setting text
|
||||||
|
const newInnerHTML = enterShouldSend ? event.target.innerHTML : amendInnerHtml(event.target.innerHTML);
|
||||||
|
setText(newInnerHTML);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[setText],
|
[setText, enterShouldSend],
|
||||||
);
|
);
|
||||||
|
|
||||||
const isCtrlEnter = useSettingValue<boolean>("MessageComposerInput.ctrlEnterToSend");
|
|
||||||
const onKeyDown = useCallback(
|
const onKeyDown = useCallback(
|
||||||
(event: KeyboardEvent<HTMLDivElement>) => {
|
(event: KeyboardEvent<HTMLDivElement>) => {
|
||||||
if (event.key === "Enter" && !event.shiftKey && (!isCtrlEnter || (isCtrlEnter && event.ctrlKey))) {
|
if (event.key === Key.ENTER) {
|
||||||
event.preventDefault();
|
const sendModifierIsPressed = IS_MAC ? event.metaKey : event.ctrlKey;
|
||||||
event.stopPropagation();
|
|
||||||
send();
|
// if enter should send, send if the user is not pushing shift
|
||||||
|
if (enterShouldSend && !event.shiftKey) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
send();
|
||||||
|
}
|
||||||
|
|
||||||
|
// if enter should not send, send only if the user is pushing ctrl/cmd
|
||||||
|
if (!enterShouldSend && sendModifierIsPressed) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
send();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[isCtrlEnter, send],
|
[enterShouldSend, send],
|
||||||
);
|
);
|
||||||
|
|
||||||
return { ref, onInput, onPaste: onInput, onKeyDown, content, setContent: setText };
|
return { ref, onInput, onPaste: onInput, onKeyDown, content, setContent: setText };
|
||||||
|
@ -14,18 +14,16 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { MutableRefObject, useCallback, useEffect, useRef } from "react";
|
import { useCallback, useEffect } from "react";
|
||||||
|
|
||||||
import useFocus from "../../../../../hooks/useFocus";
|
import useFocus from "../../../../../hooks/useFocus";
|
||||||
import { setSelection } from "../utils/selection";
|
import { useComposerContext, ComposerContextState } from "../ComposerContext";
|
||||||
|
|
||||||
type SubSelection = Pick<Selection, "anchorNode" | "anchorOffset" | "focusNode" | "focusOffset">;
|
function setSelectionContext(composerContext: ComposerContextState) {
|
||||||
|
|
||||||
function setSelectionRef(selectionRef: MutableRefObject<SubSelection>) {
|
|
||||||
const selection = document.getSelection();
|
const selection = document.getSelection();
|
||||||
|
|
||||||
if (selection) {
|
if (selection) {
|
||||||
selectionRef.current = {
|
composerContext.selection = {
|
||||||
anchorNode: selection.anchorNode,
|
anchorNode: selection.anchorNode,
|
||||||
anchorOffset: selection.anchorOffset,
|
anchorOffset: selection.anchorOffset,
|
||||||
focusNode: selection.focusNode,
|
focusNode: selection.focusNode,
|
||||||
@ -35,17 +33,12 @@ function setSelectionRef(selectionRef: MutableRefObject<SubSelection>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useSelection() {
|
export function useSelection() {
|
||||||
const selectionRef = useRef<SubSelection>({
|
const composerContext = useComposerContext();
|
||||||
anchorNode: null,
|
|
||||||
anchorOffset: 0,
|
|
||||||
focusNode: null,
|
|
||||||
focusOffset: 0,
|
|
||||||
});
|
|
||||||
const [isFocused, focusProps] = useFocus();
|
const [isFocused, focusProps] = useFocus();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function onSelectionChange() {
|
function onSelectionChange() {
|
||||||
setSelectionRef(selectionRef);
|
setSelectionContext(composerContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isFocused) {
|
if (isFocused) {
|
||||||
@ -53,15 +46,11 @@ export function useSelection() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return () => document.removeEventListener("selectionchange", onSelectionChange);
|
return () => document.removeEventListener("selectionchange", onSelectionChange);
|
||||||
}, [isFocused]);
|
}, [isFocused, composerContext]);
|
||||||
|
|
||||||
const onInput = useCallback(() => {
|
const onInput = useCallback(() => {
|
||||||
setSelectionRef(selectionRef);
|
setSelectionContext(composerContext);
|
||||||
}, []);
|
}, [composerContext]);
|
||||||
|
|
||||||
const selectPreviousSelection = useCallback(() => {
|
return { ...focusProps, onInput };
|
||||||
setSelection(selectionRef.current);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return { ...focusProps, selectPreviousSelection, onInput };
|
|
||||||
}
|
}
|
||||||
|
@ -22,9 +22,18 @@ import { ActionPayload } from "../../../../../dispatcher/payloads";
|
|||||||
import { TimelineRenderingType, useRoomContext } from "../../../../../contexts/RoomContext";
|
import { TimelineRenderingType, useRoomContext } from "../../../../../contexts/RoomContext";
|
||||||
import { useDispatcher } from "../../../../../hooks/useDispatcher";
|
import { useDispatcher } from "../../../../../hooks/useDispatcher";
|
||||||
import { focusComposer } from "./utils";
|
import { focusComposer } from "./utils";
|
||||||
|
import { ComposerType } from "../../../../../dispatcher/payloads/ComposerInsertPayload";
|
||||||
|
import { ComposerFunctions } from "../types";
|
||||||
|
import { setSelection } from "../utils/selection";
|
||||||
|
import { useComposerContext } from "../ComposerContext";
|
||||||
|
|
||||||
export function useWysiwygEditActionHandler(disabled: boolean, composerElement: RefObject<HTMLElement>) {
|
export function useWysiwygEditActionHandler(
|
||||||
|
disabled: boolean,
|
||||||
|
composerElement: RefObject<HTMLElement>,
|
||||||
|
composerFunctions: ComposerFunctions,
|
||||||
|
) {
|
||||||
const roomContext = useRoomContext();
|
const roomContext = useRoomContext();
|
||||||
|
const composerContext = useComposerContext();
|
||||||
const timeoutId = useRef<number | null>(null);
|
const timeoutId = useRef<number | null>(null);
|
||||||
|
|
||||||
const handler = useCallback(
|
const handler = useCallback(
|
||||||
@ -39,9 +48,17 @@ export function useWysiwygEditActionHandler(disabled: boolean, composerElement:
|
|||||||
case Action.FocusEditMessageComposer:
|
case Action.FocusEditMessageComposer:
|
||||||
focusComposer(composerElement, context, roomContext, timeoutId);
|
focusComposer(composerElement, context, roomContext, timeoutId);
|
||||||
break;
|
break;
|
||||||
|
case Action.ComposerInsert:
|
||||||
|
if (payload.timelineRenderingType !== roomContext.timelineRenderingType) break;
|
||||||
|
if (payload.composerType !== ComposerType.Edit) break;
|
||||||
|
|
||||||
|
if (payload.text) {
|
||||||
|
setSelection(composerContext.selection).then(() => composerFunctions.insertText(payload.text));
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[disabled, composerElement, timeoutId, roomContext],
|
[disabled, composerElement, composerFunctions, timeoutId, roomContext, composerContext],
|
||||||
);
|
);
|
||||||
|
|
||||||
useDispatcher(defaultDispatcher, handler);
|
useDispatcher(defaultDispatcher, handler);
|
||||||
|
@ -24,6 +24,8 @@ import { useDispatcher } from "../../../../../hooks/useDispatcher";
|
|||||||
import { focusComposer } from "./utils";
|
import { focusComposer } from "./utils";
|
||||||
import { ComposerFunctions } from "../types";
|
import { ComposerFunctions } from "../types";
|
||||||
import { ComposerType } from "../../../../../dispatcher/payloads/ComposerInsertPayload";
|
import { ComposerType } from "../../../../../dispatcher/payloads/ComposerInsertPayload";
|
||||||
|
import { useComposerContext } from "../ComposerContext";
|
||||||
|
import { setSelection } from "../utils/selection";
|
||||||
|
|
||||||
export function useWysiwygSendActionHandler(
|
export function useWysiwygSendActionHandler(
|
||||||
disabled: boolean,
|
disabled: boolean,
|
||||||
@ -31,6 +33,7 @@ export function useWysiwygSendActionHandler(
|
|||||||
composerFunctions: ComposerFunctions,
|
composerFunctions: ComposerFunctions,
|
||||||
) {
|
) {
|
||||||
const roomContext = useRoomContext();
|
const roomContext = useRoomContext();
|
||||||
|
const composerContext = useComposerContext();
|
||||||
const timeoutId = useRef<number | null>(null);
|
const timeoutId = useRef<number | null>(null);
|
||||||
|
|
||||||
const handler = useCallback(
|
const handler = useCallback(
|
||||||
@ -59,12 +62,12 @@ export function useWysiwygSendActionHandler(
|
|||||||
} else if (payload.event) {
|
} else if (payload.event) {
|
||||||
// TODO insert quote message - see SendMessageComposer
|
// TODO insert quote message - see SendMessageComposer
|
||||||
} else if (payload.text) {
|
} else if (payload.text) {
|
||||||
composerFunctions.insertText(payload.text);
|
setSelection(composerContext.selection).then(() => composerFunctions.insertText(payload.text));
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[disabled, composerElement, composerFunctions, timeoutId, roomContext],
|
[disabled, composerElement, roomContext, composerFunctions, composerContext],
|
||||||
);
|
);
|
||||||
|
|
||||||
useDispatcher(defaultDispatcher, handler);
|
useDispatcher(defaultDispatcher, handler);
|
||||||
|
@ -14,6 +14,9 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export { SendWysiwygComposer } from "./SendWysiwygComposer";
|
export {
|
||||||
export { EditWysiwygComposer } from "./EditWysiwygComposer";
|
DynamicImportSendWysiwygComposer as SendWysiwygComposer,
|
||||||
export { sendMessage } from "./utils/message";
|
DynamicImportEditWysiwygComposer as EditWysiwygComposer,
|
||||||
|
dynamicImportSendMessage as sendMessage,
|
||||||
|
dynamicImportConversionFunctions as getConversionFunctions,
|
||||||
|
} from "./DynamicImportWysiwygComposer";
|
||||||
|