Release notes for Android 0.12 #147
@@ -1,4 +1,66 @@
|
||||
[
|
||||
{
|
||||
"date": "2026-09-07",
|
||||
"platforms": [
|
||||
{
|
||||
"name": "android",
|
||||
"version": "0.12"
|
||||
},
|
||||
{
|
||||
"name": "relay"
|
||||
}
|
||||
],
|
||||
"headline": "Messages that would not send, and a whole lot of Android catching up.",
|
||||
"changes": [
|
||||
{
|
||||
"kind": "fixed",
|
||||
"text": "Messages to someone who had reinstalled their app could fail with “Something went wrong reaching that server” — and the server was fine. Your phone was quietly refusing to send, because it could no longer confirm the other person's keys were the ones on public record. It can again, and messages go through."
|
||||
},
|
||||
{
|
||||
"kind": "fixed",
|
||||
"text": "When your phone does refuse to send, it now says why. Before, anything it could not explain came out as a network error, which sent people off to check their signal for a decision the app had made."
|
||||
},
|
||||
{
|
||||
"kind": "fixed",
|
||||
"text": "A photo you sent as a file kept the location it was taken. Photos sent the usual way never did. Both are stripped now, on every path."
|
||||
},
|
||||
{
|
||||
"kind": "fixed",
|
||||
"text": "“Erase and start fresh”, offered when stored keys cannot be read, did not actually start fresh — it left the unreadable keys in place and came straight back to the same screen. It works now."
|
||||
},
|
||||
{
|
||||
"kind": "new",
|
||||
"text": "Reply to a particular message, so an answer that arrives an hour later still makes sense."
|
||||
},
|
||||
{
|
||||
"kind": "new",
|
||||
"text": "Files and Wallet have their own place in the bar, instead of being buried in your account."
|
||||
},
|
||||
{
|
||||
"kind": "new",
|
||||
"text": "Settings for notifications and for how the app looks, neither of which Android had."
|
||||
},
|
||||
{
|
||||
"kind": "new",
|
||||
"text": "You can report a story, which there was previously no way to do."
|
||||
},
|
||||
{
|
||||
"kind": "new",
|
||||
"text": "If a network blocks the app, you can now connect through a helper server — and hand the one you use to a contact who is stuck behind the same block."
|
||||
},
|
||||
{
|
||||
"kind": "changed",
|
||||
"text": "A new look: one dark theme that shades from purple through blue into black, a new app icon, and text you can actually read on top of it."
|
||||
},
|
||||
{
|
||||
"kind": "changed",
|
||||
"text": "The call screen is quieter — no boxed-in panel, no rings around the avatar."
|
||||
}
|
||||
],
|
||||
"selfHosting": [
|
||||
"The device directory now serves the account's address alongside its devices. Clients need it to check a published key against the transparency log on any send that does not open a new session; without it they compute the pre-address subject, match no leaf, and refuse to send. Update the relay before telling anyone to update their app."
|
||||
]
|
||||
},
|
||||
{
|
||||
"date": "2026-09-05",
|
||||
"platforms": [
|
||||
|
||||
@@ -25,6 +25,69 @@ import os
|
||||
import sys
|
||||
|
||||
|
||||
# 🔴 **The renderer decodes the whole file or none of it.** `ReleaseNotes.all`
|
||||
# fails open on a decode error — deliberately, so a bad changelog costs a page
|
||||
# rather than the site — which means ONE release with a wrong field type empties
|
||||
# every release on /releases and nothing anywhere says so. A publish gate that
|
||||
# only asked "is this version named?" therefore passed a file that renders as a
|
||||
# blank page, which is the exact outcome the gate exists to prevent. Measured
|
||||
# 2026-09-07: `selfHosting` written as a string instead of a list of strings.
|
||||
#
|
||||
# ⚠️ Every release is checked, not only the one being published. The failure is
|
||||
# not local — an unrelated older entry takes the new notes down with it.
|
||||
REQUIRED = {"date": str, "changes": list}
|
||||
OPTIONAL = {"headline": str, "selfHosting": list}
|
||||
|
||||
|
||||
def validate(releases):
|
||||
"""Every reason the Swift decoder would reject this file, in file order."""
|
||||
problems = []
|
||||
for index, release in enumerate(releases):
|
||||
where = f"release {index}"
|
||||
if not isinstance(release, dict):
|
||||
problems.append(f"{where} is not an object")
|
||||
continue
|
||||
where = f"release {release.get('date', index)}"
|
||||
|
||||
for key, want in REQUIRED.items():
|
||||
if key not in release:
|
||||
problems.append(f"{where}: no {key!r}")
|
||||
elif not isinstance(release[key], want):
|
||||
problems.append(
|
||||
f"{where}: {key!r} is {type(release[key]).__name__}, "
|
||||
f"needs {want.__name__}")
|
||||
for key, want in OPTIONAL.items():
|
||||
if key in release and not isinstance(release[key], want):
|
||||
problems.append(
|
||||
f"{where}: {key!r} is {type(release[key]).__name__}, "
|
||||
f"needs {want.__name__}")
|
||||
# `selfHosting` is a list of paragraphs; a list of anything else
|
||||
# decodes no better than a bare string.
|
||||
for note in release.get("selfHosting") or []:
|
||||
if not isinstance(note, str):
|
||||
problems.append(f"{where}: a selfHosting note is not a string")
|
||||
break
|
||||
|
||||
platforms = release.get("platforms")
|
||||
if not isinstance(platforms, list) or not platforms:
|
||||
problems.append(f"{where}: no platforms, so nobody knows what it is a release of")
|
||||
else:
|
||||
for platform in platforms:
|
||||
if not isinstance(platform, dict) or "name" not in platform:
|
||||
problems.append(f"{where}: a platform has no name")
|
||||
elif not isinstance(platform["name"], str):
|
||||
problems.append(f"{where}: a platform name is not a string")
|
||||
elif "version" in platform and not isinstance(platform["version"], str):
|
||||
problems.append(f"{where}: {platform['name']} version is not a string")
|
||||
|
||||
for change in release.get("changes") or []:
|
||||
if not isinstance(change, dict):
|
||||
problems.append(f"{where}: a change is not an object")
|
||||
elif not isinstance(change.get("kind"), str) or not isinstance(change.get("text"), str):
|
||||
problems.append(f"{where}: a change needs a string 'kind' and 'text'")
|
||||
return problems
|
||||
|
||||
|
||||
def main(argv):
|
||||
if len(argv) < 2:
|
||||
print(__doc__.strip(), file=sys.stderr)
|
||||
@@ -51,6 +114,15 @@ def main(argv):
|
||||
print(f"changelog at {path} is not a list of releases", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
problems = validate(releases)
|
||||
if problems:
|
||||
print(f"changelog at {path} will not render:", file=sys.stderr)
|
||||
for problem in problems:
|
||||
print(f" {problem}", file=sys.stderr)
|
||||
print("\nThe page decodes the WHOLE file or none of it, so this empties\n"
|
||||
"every release, not just the offending one.", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
for release in releases:
|
||||
if not isinstance(release, dict):
|
||||
continue
|
||||
|
||||
Reference in New Issue
Block a user