This commit is contained in:
MSarmadQadeer
2022-08-18 13:06:42 +00:00
parent 6a99bd9112
commit 036dea907d
16 changed files with 642 additions and 158 deletions

View File

@@ -80,22 +80,55 @@
window.addEventListener("click", (e) => {
if (e.target.tagName === "A") {
e.preventDefault();
if (e.target.href.split("#")[1]) {
const id = e.target.href.split("#")[1];
if (e.target.getAttribute("href").split("#")[0] == "") {
const id = e.target.getAttribute("href").split("#")[1];
const element = document.getElementById(id);
const yOffset = -80; // Offset for fixed header
const yOffset = 0; // Offset for fixed header
const y = element.getBoundingClientRect().top + window.pageYOffset + yOffset;
window.scrollTo({
top: y,
behavior: 'smooth'
});
} else {
window.open(e.target.href, "_blank");
}
else {
window.open(e.target.href, "_self");
}
}
});
// convert relative blog links to absolute links
document.querySelectorAll("a").forEach((a) => {
const dotSepList = a.getAttribute("href").split(".");
if (dotSepList[0] == "") {
const hashSepList = dotSepList[dotSepList.length - 1].split("#")
if (hashSepList[0] == "md") {
let str = `/blog${dotSepList[1]}`;
for (let i = 2; i < dotSepList.length; i++) {
if (dotSepList[i].substring(0, 2) != "md") {
str += "." + dotSepList[i];
} else break;
}
if (hashSepList[1]) {
str += "#" + hashSepList[1];
}
a.setAttribute("href", str);
}
}
});
const url = window.location.href;
const hash = url.split("#")[1];
if (hash) {
console.log("entered")
const element = document.getElementById(hash);
const yOffset = 0; // Offset for fixed header
const y = element.getBoundingClientRect().top + window.pageYOffset + yOffset;
window.scrollTo({
top: y,
behavior: 'smooth'
});
}
</script>
</body>

View File

@@ -88,22 +88,55 @@
window.addEventListener("click", (e) => {
if (e.target.tagName === "A") {
e.preventDefault();
if (e.target.href.split("#")[1]) {
const id = e.target.href.split("#")[1];
if (e.target.getAttribute("href").split("#")[0] == "") {
const id = e.target.getAttribute("href").split("#")[1];
const element = document.getElementById(id);
const yOffset = -80; // Offset for fixed header
const yOffset = 0; // Offset for fixed header
const y = element.getBoundingClientRect().top + window.pageYOffset + yOffset;
window.scrollTo({
top: y,
behavior: 'smooth'
});
} else {
window.open(e.target.href, "_blank");
}
else {
window.open(e.target.href, "_self");
}
}
});
// convert relative blog links to absolute links
document.querySelectorAll("a").forEach((a) => {
const dotSepList = a.getAttribute("href").split(".");
if (dotSepList[0] == "") {
const hashSepList = dotSepList[dotSepList.length - 1].split("#")
if (hashSepList[0] == "md") {
let str = `/blog${dotSepList[1]}`;
for (let i = 2; i < dotSepList.length; i++) {
if (dotSepList[i].substring(0, 2) != "md") {
str += "." + dotSepList[i];
} else break;
}
if (hashSepList[1]) {
str += "#" + hashSepList[1];
}
a.setAttribute("href", str);
}
}
});
const url = window.location.href;
const hash = url.split("#")[1];
if (hash) {
console.log("entered")
const element = document.getElementById(hash);
const yOffset = 0; // Offset for fixed header
const y = element.getBoundingClientRect().top + window.pageYOffset + yOffset;
window.scrollTo({
top: y,
behavior: 'smooth'
});
}
</script>
</body>

View File

@@ -92,22 +92,55 @@
window.addEventListener("click", (e) => {
if (e.target.tagName === "A") {
e.preventDefault();
if (e.target.href.split("#")[1]) {
const id = e.target.href.split("#")[1];
if (e.target.getAttribute("href").split("#")[0] == "") {
const id = e.target.getAttribute("href").split("#")[1];
const element = document.getElementById(id);
const yOffset = -80; // Offset for fixed header
const yOffset = 0; // Offset for fixed header
const y = element.getBoundingClientRect().top + window.pageYOffset + yOffset;
window.scrollTo({
top: y,
behavior: 'smooth'
});
} else {
window.open(e.target.href, "_blank");
}
else {
window.open(e.target.href, "_self");
}
}
});
// convert relative blog links to absolute links
document.querySelectorAll("a").forEach((a) => {
const dotSepList = a.getAttribute("href").split(".");
if (dotSepList[0] == "") {
const hashSepList = dotSepList[dotSepList.length - 1].split("#")
if (hashSepList[0] == "md") {
let str = `/blog${dotSepList[1]}`;
for (let i = 2; i < dotSepList.length; i++) {
if (dotSepList[i].substring(0, 2) != "md") {
str += "." + dotSepList[i];
} else break;
}
if (hashSepList[1]) {
str += "#" + hashSepList[1];
}
a.setAttribute("href", str);
}
}
});
const url = window.location.href;
const hash = url.split("#")[1];
if (hash) {
console.log("entered")
const element = document.getElementById(hash);
const yOffset = 0; // Offset for fixed header
const y = element.getBoundingClientRect().top + window.pageYOffset + yOffset;
window.scrollTo({
top: y,
behavior: 'smooth'
});
}
</script>
</body>

View File

@@ -87,22 +87,55 @@
window.addEventListener("click", (e) => {
if (e.target.tagName === "A") {
e.preventDefault();
if (e.target.href.split("#")[1]) {
const id = e.target.href.split("#")[1];
if (e.target.getAttribute("href").split("#")[0] == "") {
const id = e.target.getAttribute("href").split("#")[1];
const element = document.getElementById(id);
const yOffset = -80; // Offset for fixed header
const yOffset = 0; // Offset for fixed header
const y = element.getBoundingClientRect().top + window.pageYOffset + yOffset;
window.scrollTo({
top: y,
behavior: 'smooth'
});
} else {
window.open(e.target.href, "_blank");
}
else {
window.open(e.target.href, "_self");
}
}
});
// convert relative blog links to absolute links
document.querySelectorAll("a").forEach((a) => {
const dotSepList = a.getAttribute("href").split(".");
if (dotSepList[0] == "") {
const hashSepList = dotSepList[dotSepList.length - 1].split("#")
if (hashSepList[0] == "md") {
let str = `/blog${dotSepList[1]}`;
for (let i = 2; i < dotSepList.length; i++) {
if (dotSepList[i].substring(0, 2) != "md") {
str += "." + dotSepList[i];
} else break;
}
if (hashSepList[1]) {
str += "#" + hashSepList[1];
}
a.setAttribute("href", str);
}
}
});
const url = window.location.href;
const hash = url.split("#")[1];
if (hash) {
console.log("entered")
const element = document.getElementById(hash);
const yOffset = 0; // Offset for fixed header
const y = element.getBoundingClientRect().top + window.pageYOffset + yOffset;
window.scrollTo({
top: y,
behavior: 'smooth'
});
}
</script>
</body>

View File

@@ -101,22 +101,55 @@
window.addEventListener("click", (e) => {
if (e.target.tagName === "A") {
e.preventDefault();
if (e.target.href.split("#")[1]) {
const id = e.target.href.split("#")[1];
if (e.target.getAttribute("href").split("#")[0] == "") {
const id = e.target.getAttribute("href").split("#")[1];
const element = document.getElementById(id);
const yOffset = -80; // Offset for fixed header
const yOffset = 0; // Offset for fixed header
const y = element.getBoundingClientRect().top + window.pageYOffset + yOffset;
window.scrollTo({
top: y,
behavior: 'smooth'
});
} else {
window.open(e.target.href, "_blank");
}
else {
window.open(e.target.href, "_self");
}
}
});
// convert relative blog links to absolute links
document.querySelectorAll("a").forEach((a) => {
const dotSepList = a.getAttribute("href").split(".");
if (dotSepList[0] == "") {
const hashSepList = dotSepList[dotSepList.length - 1].split("#")
if (hashSepList[0] == "md") {
let str = `/blog${dotSepList[1]}`;
for (let i = 2; i < dotSepList.length; i++) {
if (dotSepList[i].substring(0, 2) != "md") {
str += "." + dotSepList[i];
} else break;
}
if (hashSepList[1]) {
str += "#" + hashSepList[1];
}
a.setAttribute("href", str);
}
}
});
const url = window.location.href;
const hash = url.split("#")[1];
if (hash) {
console.log("entered")
const element = document.getElementById(hash);
const yOffset = 0; // Offset for fixed header
const y = element.getBoundingClientRect().top + window.pageYOffset + yOffset;
window.scrollTo({
top: y,
behavior: 'smooth'
});
}
</script>
</body>

View File

@@ -45,7 +45,7 @@
<li>additional layer of E2E encryption in each message queue (to prevent traffic correlation when multiple queues are used in a conversation - something we plan later this year).</li>
<li>additional encryption of messages delivered from servers to recipients (also to prevent traffic correlation).</li>
</ul>
<p>You can read more details in our recent <a href="https://github.com/simplex-chat/simplex-chat/blob/stable/blog/20220112-simplex-chat-v1-released.md">v1 announcement</a>.</p>
<p>You can read more details in our recent <a href="./20220112-simplex-chat-v1-released.md">v1 announcement</a>.</p>
<h2>Join our public beta!</h2>
<p>Install the app <a href="https://testflight.apple.com/join/DWuT2LQu">via TestFlight</a>, connect to us (via <strong>Connect to SimpleX team</strong> link in the app) and to a couple of your friends you usually send messages to - and please let us know what you think!</p>
<p>We would really appreciate any feedback to improve the app and to decide which additional features should be included in our public release in March.</p>
@@ -99,22 +99,55 @@
window.addEventListener("click", (e) => {
if (e.target.tagName === "A") {
e.preventDefault();
if (e.target.href.split("#")[1]) {
const id = e.target.href.split("#")[1];
if (e.target.getAttribute("href").split("#")[0] == "") {
const id = e.target.getAttribute("href").split("#")[1];
const element = document.getElementById(id);
const yOffset = -80; // Offset for fixed header
const yOffset = 0; // Offset for fixed header
const y = element.getBoundingClientRect().top + window.pageYOffset + yOffset;
window.scrollTo({
top: y,
behavior: 'smooth'
});
} else {
window.open(e.target.href, "_blank");
}
else {
window.open(e.target.href, "_self");
}
}
});
// convert relative blog links to absolute links
document.querySelectorAll("a").forEach((a) => {
const dotSepList = a.getAttribute("href").split(".");
if (dotSepList[0] == "") {
const hashSepList = dotSepList[dotSepList.length - 1].split("#")
if (hashSepList[0] == "md") {
let str = `/blog${dotSepList[1]}`;
for (let i = 2; i < dotSepList.length; i++) {
if (dotSepList[i].substring(0, 2) != "md") {
str += "." + dotSepList[i];
} else break;
}
if (hashSepList[1]) {
str += "#" + hashSepList[1];
}
a.setAttribute("href", str);
}
}
});
const url = window.location.href;
const hash = url.split("#")[1];
if (hash) {
console.log("entered")
const element = document.getElementById(hash);
const yOffset = 0; // Offset for fixed header
const y = element.getBoundingClientRect().top + window.pageYOffset + yOffset;
window.scrollTo({
top: y,
behavior: 'smooth'
});
}
</script>
</body>

View File

@@ -59,7 +59,7 @@
<li>additional layer of E2E encryption in each message queue (to prevent traffic correlation when multiple queues are used in a conversation - something we plan later this year).</li>
<li>additional encryption of messages delivered from servers to recipients (also to prevent traffic correlation).</li>
</ul>
<p>You can read more technical details in our recent <a href="https://github.com/simplex-chat/simplex-chat/blob/stable/blog/20220112-simplex-chat-v1-released.md">v1 announcement</a>.</p>
<p>You can read more technical details in our recent <a href="./20220112-simplex-chat-v1-released.md">v1 announcement</a>.</p>
<p>A big thank you to <a href="https://github.com/angerman">@angerman</a> for making it possible to compile our Haskell code to mobile platforms and getting it approved on app stores - it has been a non-trivial project, and it is still ongoing.</p>
<h2>Install the apps and make a private connection!</h2>
<p>Once you install the app, you can connect to anybody:</p>
@@ -117,22 +117,55 @@
window.addEventListener("click", (e) => {
if (e.target.tagName === "A") {
e.preventDefault();
if (e.target.href.split("#")[1]) {
const id = e.target.href.split("#")[1];
if (e.target.getAttribute("href").split("#")[0] == "") {
const id = e.target.getAttribute("href").split("#")[1];
const element = document.getElementById(id);
const yOffset = -80; // Offset for fixed header
const yOffset = 0; // Offset for fixed header
const y = element.getBoundingClientRect().top + window.pageYOffset + yOffset;
window.scrollTo({
top: y,
behavior: 'smooth'
});
} else {
window.open(e.target.href, "_blank");
}
else {
window.open(e.target.href, "_self");
}
}
});
// convert relative blog links to absolute links
document.querySelectorAll("a").forEach((a) => {
const dotSepList = a.getAttribute("href").split(".");
if (dotSepList[0] == "") {
const hashSepList = dotSepList[dotSepList.length - 1].split("#")
if (hashSepList[0] == "md") {
let str = `/blog${dotSepList[1]}`;
for (let i = 2; i < dotSepList.length; i++) {
if (dotSepList[i].substring(0, 2) != "md") {
str += "." + dotSepList[i];
} else break;
}
if (hashSepList[1]) {
str += "#" + hashSepList[1];
}
a.setAttribute("href", str);
}
}
});
const url = window.location.href;
const hash = url.split("#")[1];
if (hash) {
console.log("entered")
const element = document.getElementById(hash);
const yOffset = 0; // Offset for fixed header
const y = element.getBoundingClientRect().top + window.pageYOffset + yOffset;
window.scrollTo({
top: y,
behavior: 'smooth'
});
}
</script>
</body>

View File

@@ -37,7 +37,7 @@
<div class="bg-white py-6 sm:py-12 sm:px-8 md:px-16 lg:px-20"><h1>Instant notifications for SimpleX Chat mobile apps</h1>
<p><strong>Published:</strong> April 04, 2022</p>
<h2>SimpleX Chat is the first chat platform that is 100% private by design - it has no access to your connections</h2>
<p>Since we released SimpleX Chat mobile apps couple of weeks ago we've had a lot of excitement from our users - nearly 2000 people downloaded the app after <a href="https://github.com/simplex-chat/simplex-chat/blob/stable/blog/20220308-simplex-chat-mobile-apps.md">the announcement</a>!</p>
<p>Since we released SimpleX Chat mobile apps couple of weeks ago we've had a lot of excitement from our users - nearly 2000 people downloaded the app after <a href="./20220308-simplex-chat-mobile-apps.md">the announcement</a>!</p>
<p>Huge thanks to everybody who downloaded and connected to us via the chat - there were many great questions and suggestions, and on some days I spent most of the time chatting to our users :)</p>
<p>Since we released the app, we've added and released:</p>
<ul>
@@ -76,7 +76,7 @@
<p>iOS is much more protective of what apps are allowed to run on the devices, and the solution that worked on Android is not viable on iOS.</p>
<p>We already have background refresh in the iOS app that periodically checks for new messages, and if you use the app every day it delivers notifications within 10 or 20 minutes. It is not instant, but it may be usable for some. If you use the app infrequently, however, this delay can become several hours, or your phone may stop checking for the new messages completely. This is not ideal!</p>
<p>The only solution known to us is using Apple's push notifications service (APN) to deliver push notifications.</p>
<p>We planned for it, so we added to <a href="https://github.com/simplex-chat/simplex-chat/blob/stable/blog/20220112-simplex-chat-v1-released.md">v1 of SMP</a> (the protocol used by our servers) an extension allowing the client to subscribe to notifications from message queues, via separate queue addresses, and using separate cryptographic keys for each queue. This has to be enabled by the client for each queue separately. We haven't used this extension so far, and now we are building a SimpleX notification service based on it.</p>
<p>We planned for it, so we added to <a href="./20220112-simplex-chat-v1-released.md">v1 of SMP</a> (the protocol used by our servers) an extension allowing the client to subscribe to notifications from message queues, via separate queue addresses, and using separate cryptographic keys for each queue. This has to be enabled by the client for each queue separately. We haven't used this extension so far, and now we are building a SimpleX notification service based on it.</p>
<p>If the user enables push notifications, then for each contact the app would enable a notification subscription and pass credentials to the notification server together with the device token required to deliver push notifications to user's device.</p>
<p>The notification server will subscribe to these notifications from SMP servers. The notifications do not include any message content, only the signal that a message has arrived to the server. Notification server is only allowed to send 2-3 hidden notifications per hour to the device. The notification is end-to-end encrypted and contains information about which server has a message, so that the client can connect to the server, retrieve and decrypt the message, and show the notification to the users including sender name and the message content. None of this information is shared with any server.</p>
<p>If the user receives more than 2-3 messages per hour, the notification server can send additional visible notifications that would simply say &quot;you have a new message&quot;, and the user will have to open the app to receive and see these messages. We are also investigating whether we can use &quot;mutable-content&quot; notifications that allow doing some processing when the notification arrives before showing it to the users.</p>
@@ -158,22 +158,55 @@
window.addEventListener("click", (e) => {
if (e.target.tagName === "A") {
e.preventDefault();
if (e.target.href.split("#")[1]) {
const id = e.target.href.split("#")[1];
if (e.target.getAttribute("href").split("#")[0] == "") {
const id = e.target.getAttribute("href").split("#")[1];
const element = document.getElementById(id);
const yOffset = -80; // Offset for fixed header
const yOffset = 0; // Offset for fixed header
const y = element.getBoundingClientRect().top + window.pageYOffset + yOffset;
window.scrollTo({
top: y,
behavior: 'smooth'
});
} else {
window.open(e.target.href, "_blank");
}
else {
window.open(e.target.href, "_self");
}
}
});
// convert relative blog links to absolute links
document.querySelectorAll("a").forEach((a) => {
const dotSepList = a.getAttribute("href").split(".");
if (dotSepList[0] == "") {
const hashSepList = dotSepList[dotSepList.length - 1].split("#")
if (hashSepList[0] == "md") {
let str = `/blog${dotSepList[1]}`;
for (let i = 2; i < dotSepList.length; i++) {
if (dotSepList[i].substring(0, 2) != "md") {
str += "." + dotSepList[i];
} else break;
}
if (hashSepList[1]) {
str += "#" + hashSepList[1];
}
a.setAttribute("href", str);
}
}
});
const url = window.location.href;
const hash = url.split("#")[1];
if (hash) {
console.log("entered")
const element = document.getElementById(hash);
const yOffset = 0; // Offset for fixed header
const y = element.getBoundingClientRect().top + window.pageYOffset + yOffset;
window.scrollTo({
top: y,
behavior: 'smooth'
});
}
</script>
</body>

View File

@@ -41,7 +41,7 @@
<p>How does it work? The gallery and files are accessed from a system provided dialogue that runs in a separate process, and provides a temporary URI to access only one file selected by the user, only until the app is restarted.</p>
<p>To make file and images work for mobile apps we made a breaking change in SimpleX Chat core. The current version can exchange files with the previous version 1.6 of the terminal app, but not with the version before that.</p>
<p>In the mobile app, to send and receive files both devices must have version 2.0 installed - so please check it with your contacts. Receiving images works in the previous version, so even if your contacts did not yet upgrade the app, they should be able to receive the images.</p>
<h2>The first messaging platform without user identifiers.</h2>
<h2>The first messaging platform without user identifiers</h2>
<p>To protect identities of users and their connections, SimpleX Chat has no user identifiers visible to the network unlike any other messaging platform.</p>
<p>Many people asked: <em>if SimpleX has no user identifiers, how can it deliver messages?</em></p>
<p>To deliver mesages, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts. In the current version of the protocol each queue is used until the contact is deleted. Later this year we plan to add queue rotation to the client protocol, so that even conversations don't have long term identifiers visible to the network. This design prevents leaking any users metadata on the application level.</p>
@@ -93,22 +93,55 @@
window.addEventListener("click", (e) => {
if (e.target.tagName === "A") {
e.preventDefault();
if (e.target.href.split("#")[1]) {
const id = e.target.href.split("#")[1];
if (e.target.getAttribute("href").split("#")[0] == "") {
const id = e.target.getAttribute("href").split("#")[1];
const element = document.getElementById(id);
const yOffset = -80; // Offset for fixed header
const yOffset = 0; // Offset for fixed header
const y = element.getBoundingClientRect().top + window.pageYOffset + yOffset;
window.scrollTo({
top: y,
behavior: 'smooth'
});
} else {
window.open(e.target.href, "_blank");
}
else {
window.open(e.target.href, "_self");
}
}
});
// convert relative blog links to absolute links
document.querySelectorAll("a").forEach((a) => {
const dotSepList = a.getAttribute("href").split(".");
if (dotSepList[0] == "") {
const hashSepList = dotSepList[dotSepList.length - 1].split("#")
if (hashSepList[0] == "md") {
let str = `/blog${dotSepList[1]}`;
for (let i = 2; i < dotSepList.length; i++) {
if (dotSepList[i].substring(0, 2) != "md") {
str += "." + dotSepList[i];
} else break;
}
if (hashSepList[1]) {
str += "#" + hashSepList[1];
}
a.setAttribute("href", str);
}
}
});
const url = window.location.href;
const hash = url.split("#")[1];
if (hash) {
console.log("entered")
const element = document.getElementById(hash);
const yOffset = 0; // Offset for fixed header
const y = element.getBoundingClientRect().top + window.pageYOffset + yOffset;
window.scrollTo({
top: y,
behavior: 'smooth'
});
}
</script>
</body>

View File

@@ -40,8 +40,8 @@
<p>In this version you can irreversibly delete individual messages after they were deleted by a sender, and also completely clear the conversation.</p>
<p>The only way to do it before this version was by deleting the contact, now you can keep the connection when you clear the conversation.</p>
<p><img src="/img/images/20220524-clear-chat1.png" width="240"> <img src="/img/images/20220524-clear-chat2.png" width="240"></p>
<p>See <a href="https://github.com/simplex-chat/simplex-chat/blob/stable/blog/20220112-simplex-chat-v1-released.md">v1 announcement</a> for information on how SimpleX protects the security of the messages.</p>
<p>See <a href="https://github.com/simplex-chat/simplex-chat/blob/stable/blog/20220511-simplex-chat-v2-images-files.md">v2 announcement</a> for more information about SimpleX platform and how it works.</p>
<p>See <a href="./20220112-simplex-chat-v1-released.md">v1 announcement</a> for information on how SimpleX protects the security of the messages.</p>
<p>See <a href="./20220511-simplex-chat-v2-images-files.md">v2 announcement</a> for more information about SimpleX platform and how it works.</p>
<p>Read about SimpleX design in <a href="https://github.com/simplex-chat/simplexmq/blob/master/protocol/overview-tjr.md">whitepaper</a>.</p>
</div>
</section>
@@ -80,22 +80,55 @@
window.addEventListener("click", (e) => {
if (e.target.tagName === "A") {
e.preventDefault();
if (e.target.href.split("#")[1]) {
const id = e.target.href.split("#")[1];
if (e.target.getAttribute("href").split("#")[0] == "") {
const id = e.target.getAttribute("href").split("#")[1];
const element = document.getElementById(id);
const yOffset = -80; // Offset for fixed header
const yOffset = 0; // Offset for fixed header
const y = element.getBoundingClientRect().top + window.pageYOffset + yOffset;
window.scrollTo({
top: y,
behavior: 'smooth'
});
} else {
window.open(e.target.href, "_blank");
}
else {
window.open(e.target.href, "_self");
}
}
});
// convert relative blog links to absolute links
document.querySelectorAll("a").forEach((a) => {
const dotSepList = a.getAttribute("href").split(".");
if (dotSepList[0] == "") {
const hashSepList = dotSepList[dotSepList.length - 1].split("#")
if (hashSepList[0] == "md") {
let str = `/blog${dotSepList[1]}`;
for (let i = 2; i < dotSepList.length; i++) {
if (dotSepList[i].substring(0, 2) != "md") {
str += "." + dotSepList[i];
} else break;
}
if (hashSepList[1]) {
str += "#" + hashSepList[1];
}
a.setAttribute("href", str);
}
}
});
const url = window.location.href;
const hash = url.split("#")[1];
if (hash) {
console.log("entered")
const element = document.getElementById(hash);
const yOffset = 0; // Offset for fixed header
const y = element.getBoundingClientRect().top + window.pageYOffset + yOffset;
window.scrollTo({
top: y,
behavior: 'smooth'
});
}
</script>
</body>

View File

@@ -36,7 +36,7 @@
<section class="container">
<div class="bg-white py-6 sm:py-12 sm:px-8 md:px-16 lg:px-20"><h1>SimpleX Chat v2.2 - the first messaging platform without user identities - 100% private by design!</h1>
<p><strong>Published:</strong> June 4, 2022</p>
<p>See <a href="https://github.com/simplex-chat/simplex-chat/blob/stable/blog/20220511-simplex-chat-v2-images-files.md">v2 announcement</a> for more information about SimpleX platform and how it protects your privacy by avoiding user identities of any kind in its design - SimpleX, unlike any other messaging platfom, has no identity keys or any numbers that identify its users.</p>
<p>See <a href="./20220511-simplex-chat-v2-images-files.md">v2 announcement</a> for more information about SimpleX platform and how it protects your privacy by avoiding user identities of any kind in its design - SimpleX, unlike any other messaging platform, has no identity keys or any numbers that identify its users.</p>
<h2>New Privacy and Security settings in version 2.2</h2>
<img src="/img/images/20220604-privacy-settings.png" width="480">
<h3>Protect your chats</h3>
@@ -59,7 +59,7 @@
<h3>There is more</h3>
<p>You can discover additional features we are currently testing in Experimental Features - they will be announced later!</p>
<h2>More information</h2>
<p>See <a href="https://github.com/simplex-chat/simplex-chat/blob/stable/blog/20220112-simplex-chat-v1-released.md">v1 announcement</a> for information on how SimpleX protects the security of the messages.</p>
<p>See <a href="./20220112-simplex-chat-v1-released.md">v1 announcement</a> for information on how SimpleX protects the security of the messages.</p>
<p>Read about SimpleX design in <a href="https://github.com/simplex-chat/simplexmq/blob/master/protocol/overview-tjr.md">whitepaper</a>.</p>
</div>
</section>
@@ -98,22 +98,55 @@
window.addEventListener("click", (e) => {
if (e.target.tagName === "A") {
e.preventDefault();
if (e.target.href.split("#")[1]) {
const id = e.target.href.split("#")[1];
if (e.target.getAttribute("href").split("#")[0] == "") {
const id = e.target.getAttribute("href").split("#")[1];
const element = document.getElementById(id);
const yOffset = -80; // Offset for fixed header
const yOffset = 0; // Offset for fixed header
const y = element.getBoundingClientRect().top + window.pageYOffset + yOffset;
window.scrollTo({
top: y,
behavior: 'smooth'
});
} else {
window.open(e.target.href, "_blank");
}
else {
window.open(e.target.href, "_self");
}
}
});
// convert relative blog links to absolute links
document.querySelectorAll("a").forEach((a) => {
const dotSepList = a.getAttribute("href").split(".");
if (dotSepList[0] == "") {
const hashSepList = dotSepList[dotSepList.length - 1].split("#")
if (hashSepList[0] == "md") {
let str = `/blog${dotSepList[1]}`;
for (let i = 2; i < dotSepList.length; i++) {
if (dotSepList[i].substring(0, 2) != "md") {
str += "." + dotSepList[i];
} else break;
}
if (hashSepList[1]) {
str += "#" + hashSepList[1];
}
a.setAttribute("href", str);
}
}
});
const url = window.location.href;
const hash = url.split("#")[1];
if (hash) {
console.log("entered")
const element = document.getElementById(hash);
const yOffset = 0; // Offset for fixed header
const y = element.getBoundingClientRect().top + window.pageYOffset + yOffset;
window.scrollTo({
top: y,
behavior: 'smooth'
});
}
</script>
</body>

View File

@@ -43,7 +43,7 @@
<li><a href="#protocol-privacy-and-performance-improvements">protocol privacy and performance improvements</a></li>
</ul>
<h3>Instant notifications for iOS</h3>
<p>I wrote previously about <a href="https://github.com/simplex-chat/simplex-chat/blob/stable/blog/20220404-simplex-chat-instant-notifications.md#problem---users-expect-to-be-instantly-notified-when-messages-arrive">our design for iOS notifications</a> - this is now released. The app will offer to migrate the database when updated, and then you need to choose notifications mode instant or periodic push notifications, or previously available periodic background refresh that does not use push notifications.</p>
<p>I wrote previously about <a href="./20220404-simplex-chat-instant-notifications.md#problem---users-expect-to-be-instantly-notified-when-messages-arrive!">our design for iOS notifications</a> - this is now released. The app will offer to migrate the database when updated, and then you need to choose notifications mode instant or periodic push notifications, or previously available periodic background refresh that does not use push notifications.</p>
<p>To deliver the notifications to iOS devices we use our notification server, as there is a single private key that Apple issues for the app. This server has minimal amount of information about your chat activity:</p>
<ul>
<li>it does not have the addresses of messaging queues used to send and receive messages - there is an additional address used for notification server to receive notifications from the messaging servers.</li>
@@ -73,7 +73,7 @@
<h3>Protocol privacy and performance improvements</h3>
<p>Adding push notifications for iOS required SimpleX Messaging Protocol changes. We managed not just to keep the same level of meta-data privacy from passive observers, but to improve it - now all message meta-data that is passed from the server to the recipient is included into the same encrypted envelope as the message itself - as before, there is no identifiers or ciphertext in common inside TLS traffic between received and sent traffic of the server, and now there is no message timestamp inside TLS as well.</p>
<p>We also improved the protocol flow for establishing bidirectional connection between two users - it is substantially faster now, consuming much less network traffic and battery. It improves the time it takes to connect to your contacts and to start delivering images and files.</p>
<p>All these changes did not affect backward compatibility - if your contact has the previous version of the client, or you are connecting to a previous version of the server, the previous version of the protocol will be used - SimpleX has independent version negotiation in 4 protocol layers <a href="https://github.com/simplex-chat/simplex-chat/blob/stable/blog/20220112-simplex-chat-v1-released.md#stable-protocol-implementation">since v1</a>, allowing us to evolve the protocols without any disruption to the users.</p>
<p>All these changes did not affect backward compatibility - if your contact has the previous version of the client, or you are connecting to a previous version of the server, the previous version of the protocol will be used - SimpleX has independent version negotiation in 4 protocol layers <a href="./20220112-simplex-chat-v1-released.md#stable-protocol-implementation">since v1</a>, allowing us to evolve the protocols without any disruption to the users.</p>
<h2>SimpleX platform</h2>
<p>We are building a new platform for distributed Internet applications where privacy of the messages <em>and</em> the network matter. <a href="https://github.com/simplex-chat/simplex-chat">SimpleX Chat</a> is our first application, a messaging application built on the SimpleX platform.</p>
<h3>The first (and we believe the only) messaging platform without user identifiers of any kind - 100% private by design!</h3>
@@ -83,7 +83,7 @@
<p>SimpleX platform avoids these risks by not having any user identity in its design - so even if you talk to two different people from the same chat profile, they would not be able to prove they are talking to the same person - only that user profiles look the same. And we are planning to add a feature allowing to have a different display name for each contact you connect to - quite a few users asked for it.</p>
<h3>How does it work</h3>
<p>Many people asked: <em>if SimpleX has no user identifiers, how can it deliver messages?</em></p>
<p>I wrote about it in <a href="https://github.com/simplex-chat/simplex-chat/blob/stable/blog/20220511-simplex-chat-v2-images-files.md">v2 release announcement</a> and you can get more information about SimpleX platform objectives and technical design in <a href="https://github.com/simplex-chat/simplexmq/blob/master/protocol/overview-tjr.md">the whitepaper</a>.</p>
<p>I wrote about it in <a href="./20220511-simplex-chat-v2-images-files.md">v2 release announcement</a> and you can get more information about SimpleX platform objectives and technical design in <a href="https://github.com/simplex-chat/simplexmq/blob/master/protocol/overview-tjr.md">the whitepaper</a>.</p>
<h2>We ask you to help us pay for 3rd party security audit</h2>
<p>I will get straight to the point: I ask you to support SimpleX Chat with donations.</p>
<p>We are prioritizing users privacy and security - it would be impossible without your support we were lucky to have so far.</p>
@@ -131,22 +131,55 @@
window.addEventListener("click", (e) => {
if (e.target.tagName === "A") {
e.preventDefault();
if (e.target.href.split("#")[1]) {
const id = e.target.href.split("#")[1];
if (e.target.getAttribute("href").split("#")[0] == "") {
const id = e.target.getAttribute("href").split("#")[1];
const element = document.getElementById(id);
const yOffset = -80; // Offset for fixed header
const yOffset = 0; // Offset for fixed header
const y = element.getBoundingClientRect().top + window.pageYOffset + yOffset;
window.scrollTo({
top: y,
behavior: 'smooth'
});
} else {
window.open(e.target.href, "_blank");
}
else {
window.open(e.target.href, "_self");
}
}
});
// convert relative blog links to absolute links
document.querySelectorAll("a").forEach((a) => {
const dotSepList = a.getAttribute("href").split(".");
if (dotSepList[0] == "") {
const hashSepList = dotSepList[dotSepList.length - 1].split("#")
if (hashSepList[0] == "md") {
let str = `/blog${dotSepList[1]}`;
for (let i = 2; i < dotSepList.length; i++) {
if (dotSepList[i].substring(0, 2) != "md") {
str += "." + dotSepList[i];
} else break;
}
if (hashSepList[1]) {
str += "#" + hashSepList[1];
}
a.setAttribute("href", str);
}
}
});
const url = window.location.href;
const hash = url.split("#")[1];
if (hash) {
console.log("entered")
const element = document.getElementById(hash);
const yOffset = 0; // Offset for fixed header
const y = element.getBoundingClientRect().top + window.pageYOffset + yOffset;
window.scrollTo({
top: y,
behavior: 'smooth'
});
}
</script>
</body>

View File

@@ -76,9 +76,9 @@
<h2>SimpleX platform</h2>
<p>We are building a new platform for distributed Internet applications where privacy of the messages <em>and</em> the network matter. <a href="https://github.com/simplex-chat/simplex-chat">SimpleX Chat</a> is our first application, a messaging application built on the SimpleX platform.</p>
<h3>The first (and the only?) messaging platform without user identifiers of any kind - 100% private by design!</h3>
<p>To protect identities of users and their connections, instead of user identifiers visible to the servers and/or the network (that are used by all other messaging platforms), SimpleX Chat uses <a href="https://csrc.nist.gov/glossary/term/Pairwise_Pseudonymous_Identifier">pairwise identifiers</a> of connections between the users there are two queues in each connection, each queue having 2 different identifiers to send and to receive the messages. It increases the number of used identifiers to the square of the number of users, making it more difficult (or impossible) to determine who is talking to whom. I <a href="https://github.com/simplex-chat/simplex-chat/blob/stable/blog/20220711-simplex-chat-v3-released-ios-notifications-audio-video-calls-database-export-import-protocol-improvements.md#why-having-users-identifiers-is-bad-for-the-users">wrote previously</a> why it is bad for the users' privacy to have any identifiers, even random numbers, associated with their profiles.</p>
<p>To protect identities of users and their connections, instead of user identifiers visible to the servers and/or the network (that are used by all other messaging platforms), SimpleX Chat uses <a href="https://csrc.nist.gov/glossary/term/Pairwise_Pseudonymous_Identifier">pairwise identifiers</a> of connections between the users there are two queues in each connection, each queue having 2 different identifiers to send and to receive the messages. It increases the number of used identifiers to the square of the number of users, making it more difficult (or impossible) to determine who is talking to whom. I <a href="./20220711-simplex-chat-v3-released-ios-notifications-audio-video-calls-database-export-import-protocol-improvements.md#why-having-users'-identifiers-is-bad-for-the-users?">wrote previously</a> why it is bad for the users' privacy to have any identifiers, even random numbers, associated with their profiles.</p>
<h3>If SimpleX has no user identifiers, how can it deliver messages?</h3>
<p>I wrote about it in <a href="https://github.com/simplex-chat/simplex-chat/blob/stable/blog/20220511-simplex-chat-v2-images-files.md">v2 release announcement</a> and you can get more information about SimpleX platform objectives and technical design in <a href="https://github.com/simplex-chat/simplexmq/blob/master/protocol/overview-tjr.md">the whitepaper</a>.</p>
<p>I wrote about it in <a href="./20220511-simplex-chat-v2-images-files.md">v2 release announcement</a> and you can get more information about SimpleX platform objectives and technical design in <a href="https://github.com/simplex-chat/simplexmq/blob/master/protocol/overview-tjr.md">the whitepaper</a>.</p>
<h3>Privacy: technical details and limitations</h3>
<p>SimpleX design follows &quot;defence in depth&quot; security principles having multiple overlapping defensive mechanisms to protect users privacy and security:</p>
<ul>
@@ -148,22 +148,55 @@
window.addEventListener("click", (e) => {
if (e.target.tagName === "A") {
e.preventDefault();
if (e.target.href.split("#")[1]) {
const id = e.target.href.split("#")[1];
if (e.target.getAttribute("href").split("#")[0] == "") {
const id = e.target.getAttribute("href").split("#")[1];
const element = document.getElementById(id);
const yOffset = -80; // Offset for fixed header
const yOffset = 0; // Offset for fixed header
const y = element.getBoundingClientRect().top + window.pageYOffset + yOffset;
window.scrollTo({
top: y,
behavior: 'smooth'
});
} else {
window.open(e.target.href, "_blank");
}
else {
window.open(e.target.href, "_self");
}
}
});
// convert relative blog links to absolute links
document.querySelectorAll("a").forEach((a) => {
const dotSepList = a.getAttribute("href").split(".");
if (dotSepList[0] == "") {
const hashSepList = dotSepList[dotSepList.length - 1].split("#")
if (hashSepList[0] == "md") {
let str = `/blog${dotSepList[1]}`;
for (let i = 2; i < dotSepList.length; i++) {
if (dotSepList[i].substring(0, 2) != "md") {
str += "." + dotSepList[i];
} else break;
}
if (hashSepList[1]) {
str += "#" + hashSepList[1];
}
a.setAttribute("href", str);
}
}
});
const url = window.location.href;
const hash = url.split("#")[1];
if (hash) {
console.log("entered")
const element = document.getElementById(hash);
const yOffset = 0; // Offset for fixed header
const y = element.getBoundingClientRect().top + window.pageYOffset + yOffset;
window.scrollTo({
top: y,
behavior: 'smooth'
});
}
</script>
</body>

View File

@@ -47,7 +47,7 @@
</ul>
<h3>Secret chat groups</h3>
<p><img src="/img/images/20220808-group1.png" width="330"> <img src="/img/images/20220808-group2.png" width="330"> <img src="/img/images/20220808-group3.png" width="330"></p>
<p>It's been <a href="https://github.com/simplex-chat/simplex-chat/blob/stable/blog/20210914-simplex-chat-v0.4-released.md">nearly a year</a> since the users of SimpleX Chat terminal app started experimenting with the groups, and now it is available to mobile app users as well. Many bugs were fixed, the stability was improved, but there are both the features we need to add and the bugs we need to fix to make groups more useful - we really look forward to your feedback. You can send any suggestions via the app by choosing <code>Chat with the developers</code> via app Settings (or using <code>/simplex</code> command in the terminal app) this would connect you to SimpleX team via its <a href="https://simplex.chat/contact#/?v=1&amp;smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D">fixed chat address</a>.</p>
<p>It's been <a href="./20210914-simplex-chat-v0.4-released.md">nearly a year</a> since the users of SimpleX Chat terminal app started experimenting with the groups, and now it is available to mobile app users as well. Many bugs were fixed, the stability was improved, but there are both the features we need to add and the bugs we need to fix to make groups more useful - we really look forward to your feedback. You can send any suggestions via the app by choosing <code>Chat with the developers</code> via app Settings (or using <code>/simplex</code> command in the terminal app) this would connect you to SimpleX team via its <a href="https://simplex.chat/contact#/?v=1&amp;smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D">fixed chat address</a>.</p>
<p>SimpleX network is decentralized, so how do groups work? Unlike Matrix or Signal that host the group profile and the list of group members on their servers, SimpleX servers have no information about the group's existence - only its members do. SimpleX network does not assign any globally unique identifiers to the group, there is only a local database identifier and the list of members stored on members' devices. A user has an independent connection to each member in a group. When a user sends a message to the group, the app sends this message independently to each member. You can read more about how groups work in <a href="https://github.com/simplex-chat/simplex-chat/blob/stable/docs/protocol/simplex-chat.md#sub-protocol-for-chat-groups">SimpleX Chat Protocol</a>.</p>
<p>But how can it scale, you might ask? It simply won't, and the current design for the groups is only suitable for relatively small groups of people who know each other well, definitely not larger than few hundred members this design prioritized privacy and security of the group over its size or performance. For example, to send a message to the group of 100 members a user would need to send a total of ~1.6mb of data (as each message uses a fixed size block of 16kb). And if you were to send a 1mb file then it would also require sending it 100 times (provided each member accepts it).</p>
<p>What if you need to send many large files to group members? We will be developing a file hosting server where the users will be able to upload the file (or image) once, and only send the file link and credentials to all group members, without the need to send the actual file. A small hosting quota will be available to all users for free, paid for by donations, and for larger files or to increase the total quota the users would either have to pay a small hosting cost or to self-host this server it will be available as an open-source code.</p>
@@ -83,12 +83,12 @@
<p>The <a href="https://github.com/simplex-chat/simplexmq/blob/stable/protocol/">low level SimpleX protocols</a> were published long time ago, and updated to reflect the evolution of the protocols, the high level chat protocol was not published before. The reason for that was to allow us to iterate it quickly, without committing to any of the decisions.</p>
<p>This is the <a href="https://github.com/simplex-chat/simplex-chat/blob/stable/docs/protocol/simplex-chat.md">first draft of SimpleX Chat Protocol</a> - let us know any questions or suggestions.</p>
<h3>Other changes since v3</h3>
<p>Since v3 release we also optimized battery and traffic usage - with up to 90x traffic reduction in some cases and published two docker configurations for self-hosted SMP servers. Read more about it in the previous <a href="https://github.com/simplex-chat/simplex-chat/blob/stable/blog/20220723-simplex-chat-v3.1-tor-groups-efficiency.md">beta version announcement</a>.</p>
<p>Since v3 release we also optimized battery and traffic usage - with up to 90x traffic reduction in some cases and published two docker configurations for self-hosted SMP servers. Read more about it in the previous <a href="./20220723-simplex-chat-v3.1-tor-groups-efficiency.md">beta version announcement</a>.</p>
<h2>SimpleX platform</h2>
<p>Some links to answer the most common questions:</p>
<p><a href="https://github.com/simplex-chat/simplex-chat/blob/stable/blog/20220511-simplex-chat-v2-images-files.md#the-first-messaging-platform-without-user-identifiers">How can SimpleX deliver messages without user identifiers</a>.</p>
<p><a href="https://github.com/simplex-chat/simplex-chat/blob/stable/blog/20220711-simplex-chat-v3-released-ios-notifications-audio-video-calls-database-export-import-protocol-improvements.md#why-having-users-identifiers-is-bad-for-the-users">What are the risks to have identifiers assigned to the users</a>.</p>
<p><a href="https://github.com/simplex-chat/simplex-chat/blob/stable/blog/20220723-simplex-chat-v3.1-tor-groups-efficiency.md#privacy-technical-details-and-limitations">Technical details and limitations</a>.</p>
<p><a href="./20220511-simplex-chat-v2-images-files.md#the-first-messaging-platform-without-user-identifiers">How can SimpleX deliver messages without user identifiers</a>.</p>
<p><a href="./20220711-simplex-chat-v3-released-ios-notifications-audio-video-calls-database-export-import-protocol-improvements.md#why-having-users'-identifiers-is-bad-for-the-users?">What are the risks to have identifiers assigned to the users</a>.</p>
<p><a href="./20220723-simplex-chat-v3.1-tor-groups-efficiency.md#privacy:-technical-details-and-limitations">Technical details and limitations</a>.</p>
<p><a href="https://github.com/simplex-chat/simplex-chat/blob/stable/README.md#frequently-asked-questions">How SimpleX is different from Session, Matrix, Signal, etc.</a>.</p>
<h2>We ask you to help us pay for 3rd party security audit</h2>
<p>I will get straight to the point: I ask you to support SimpleX Chat with donations.</p>
@@ -142,22 +142,55 @@
window.addEventListener("click", (e) => {
if (e.target.tagName === "A") {
e.preventDefault();
if (e.target.href.split("#")[1]) {
const id = e.target.href.split("#")[1];
if (e.target.getAttribute("href").split("#")[0] == "") {
const id = e.target.getAttribute("href").split("#")[1];
const element = document.getElementById(id);
const yOffset = -80; // Offset for fixed header
const yOffset = 0; // Offset for fixed header
const y = element.getBoundingClientRect().top + window.pageYOffset + yOffset;
window.scrollTo({
top: y,
behavior: 'smooth'
});
} else {
window.open(e.target.href, "_blank");
}
else {
window.open(e.target.href, "_self");
}
}
});
// convert relative blog links to absolute links
document.querySelectorAll("a").forEach((a) => {
const dotSepList = a.getAttribute("href").split(".");
if (dotSepList[0] == "") {
const hashSepList = dotSepList[dotSepList.length - 1].split("#")
if (hashSepList[0] == "md") {
let str = `/blog${dotSepList[1]}`;
for (let i = 2; i < dotSepList.length; i++) {
if (dotSepList[i].substring(0, 2) != "md") {
str += "." + dotSepList[i];
} else break;
}
if (hashSepList[1]) {
str += "#" + hashSepList[1];
}
a.setAttribute("href", str);
}
}
});
const url = window.location.href;
const hash = url.split("#")[1];
if (hash) {
console.log("entered")
const element = document.getElementById(hash);
const yOffset = 0; // Offset for fixed header
const y = element.getBoundingClientRect().top + window.pageYOffset + yOffset;
window.scrollTo({
top: y,
behavior: 'smooth'
});
}
</script>
</body>

View File

@@ -1,58 +1,76 @@
h1{
font-size: 1.8rem;
font-weight: 600;
letter-spacing: 0.6px;
h1 {
font-size: 1.8rem;
font-weight: 600;
letter-spacing: 0.6px;
}
section.container > div > p:nth-child(2){
margin: 0;
margin-bottom: 2rem;
margin-top: 4px;
font-size: 1rem;
section.container > div > p:nth-child(2) {
margin: 0;
margin-bottom: 2rem;
margin-top: 4px;
font-size: 1rem;
}
h2{
font-size: 1.6rem;
font-weight: 600;
letter-spacing: 0.6px;
margin-bottom: 1rem;
margin-top: 2.2rem;
h2 {
font-size: 1.6rem;
font-weight: 600;
letter-spacing: 0.6px;
margin-bottom: 1rem;
margin-top: 2.2rem;
}
h3{
font-size: 1.2rem;
font-weight: 600;
letter-spacing: 0.6px;
margin-bottom: 1rem;
margin-top: 1.8rem;
h3 {
font-size: 1.2rem;
font-weight: 600;
letter-spacing: 0.6px;
margin-bottom: 1rem;
margin-top: 1.8rem;
}
ul li,ol li{
text-decoration: none;
/* list-style: none; */
font-size: 1.1rem;
line-height: 30px;
letter-spacing: 0.6px;
margin: 0.5rem 0;
ul li,
ol li {
text-decoration: none;
/* list-style: none; */
font-size: 1.1rem;
line-height: 30px;
letter-spacing: 0.6px;
margin: 0.5rem 0;
}
ul,ol{
list-style-position: inside;
/* list-style-type: decimal; */
overflow: auto;
ul,
ol {
list-style-position: inside;
/* list-style-type: decimal; */
overflow: auto;
}
ul li::marker, ol li::marker{
/* content: "-> "; */
font-weight: 600;
/* color: #fbd561; */
ul li::marker,
ol li::marker {
/* content: "-> "; */
font-weight: 600;
/* color: #fbd561; */
}
pre{
overflow: auto;
pre {
overflow: auto;
}
/* code{
width: 100%;
height: auto;
} */
p{
margin: 1rem 0;
}
p {
margin: 1rem 0;
}
html {
scroll-behavior: smooth;
}
h1::before,
h2::before,
h3::before {
display: block;
content: " ";
margin-top: -80px;
height: 80px;
visibility: hidden;
pointer-events: none;
}

View File

@@ -50,6 +50,10 @@
position: absolute
}
.relative {
position: relative
}
.m-4 {
margin: 1rem
}