Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
204 changes: 204 additions & 0 deletions apps/swift-ios/Features/Settings/BuildChangelog.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
import Foundation
import SwiftUI

struct BuildChangelog: Codable, Equatable, Sendable {
struct Entry: Codable, Equatable, Identifiable, Sendable {
let commit: String
let title: String
let summary: String
let pullRequest: Int?
let pullRequestURL: URL?
let committedAt: Date?

var id: String { commit }
var shortCommit: String { String(commit.prefix(7)) }
var displaySummary: String? {
let value = summary.trimmingCharacters(in: .whitespacesAndNewlines)
guard !value.isEmpty,
value.localizedCaseInsensitiveCompare(
title.trimmingCharacters(in: .whitespacesAndNewlines)
) != .orderedSame else {
return nil
}
return value
}
}

let revision: String
let baseRevision: String?
let repositoryURL: URL?
let generatedBy: String
let entries: [Entry]

static let embedded = load(info: Bundle.main.infoDictionary)

static func load(info: [String: Any]?) -> BuildChangelog? {
guard let encoded = info?["T3BuildChangelog"] as? String,
!encoded.isEmpty,
!encoded.hasPrefix("$("),
let data = Data(base64Encoded: encoded)
else { return nil }

let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
return try? decoder.decode(BuildChangelog.self, from: data)
}
}

struct BuildChangelogView: View {
let changelog: BuildChangelog?
let versionLabel: String

var body: some View {
ScrollView {
LazyVStack(alignment: .leading, spacing: 0) {
header.padding(.bottom, 24)

if let changelog, !changelog.entries.isEmpty {
if let latest = changelog.entries.last {
latestChange(latest, repositoryURL: changelog.repositoryURL)
.padding(.bottom, 28)
}

let earlierEntries = Array(changelog.entries.dropLast().reversed())
if !earlierEntries.isEmpty {
Text("Earlier in this build")
.font(T3Typography.homeTitle)
.foregroundStyle(T3Colors.textPrimary)
.padding(.bottom, 16)
}
ForEach(Array(earlierEntries.enumerated()), id: \.element.id) { index, entry in
milestone(
entry,
repositoryURL: changelog.repositoryURL,
isLast: index == earlierEntries.count - 1
)
}
} else if changelog == nil {
ContentUnavailableView(
"No build changelog",
systemImage: "list.bullet.rectangle",
description: Text("This build did not include an embedded changelog.")
)
Comment thread
saphid marked this conversation as resolved.
.frame(maxWidth: .infinity)
.padding(.top, 48)
} else {
ContentUnavailableView(
"No changes in this build",
systemImage: "checkmark.circle",
description: Text(
changelog?.baseRevision == nil
? "This build did not configure a base revision for comparison."
: "No commits were found between this build and its configured base revision."
)
)
.frame(maxWidth: .infinity)
.padding(.top, 48)
}
}
.padding(20)
}
.background(T3Colors.background)
.navigationTitle("What’s in this build")
.navigationBarTitleDisplayMode(.inline)
}

private var header: some View {
VStack(alignment: .leading, spacing: 6) {
Text("Version \(versionLabel)")
.font(T3Typography.homeTitle)
.foregroundStyle(T3Colors.textPrimary)
if let changelog {
Text("Revision \(String(changelog.revision.prefix(7))) · Summaries by \(changelog.generatedBy)")
.font(T3Typography.supporting)
.foregroundStyle(T3Colors.textSecondary)
}
}
.frame(maxWidth: .infinity, alignment: .leading)
}

private func latestChange(_ entry: BuildChangelog.Entry, repositoryURL: URL?) -> some View {
VStack(alignment: .leading, spacing: 10) {
Label("New in this version", systemImage: "sparkles")
.font(T3Typography.homeTitle)
.foregroundStyle(T3Colors.accent)
Text(entry.title)
.font(T3Typography.threadBody)
.fontWeight(.semibold)
.foregroundStyle(T3Colors.textPrimary)
if let summary = entry.displaySummary {
Text(summary)
.font(T3Typography.supporting)
.foregroundStyle(T3Colors.textSecondary)
.fixedSize(horizontal: false, vertical: true)
}
changeLinks(entry, repositoryURL: repositoryURL)
}
.padding(16)
.frame(maxWidth: .infinity, alignment: .leading)
.background(T3Colors.surface, in: RoundedRectangle(cornerRadius: 14))
.overlay {
RoundedRectangle(cornerRadius: 14)
.stroke(T3Colors.border, lineWidth: 1)
}
}

private func milestone(
_ entry: BuildChangelog.Entry,
repositoryURL: URL?,
isLast: Bool
) -> some View {
HStack(alignment: .top, spacing: 14) {
VStack(spacing: 0) {
Circle()
.fill(T3Colors.accent)
.frame(width: 10, height: 10)
.padding(.top, 5)
if !isLast {
Rectangle()
.fill(T3Colors.border)
.frame(width: 2)
.frame(minHeight: 92)
}
}

VStack(alignment: .leading, spacing: 6) {
Text(entry.title)
.font(T3Typography.threadBody)
.fontWeight(.semibold)
.foregroundStyle(T3Colors.textPrimary)
if let summary = entry.displaySummary {
Text(summary)
.font(T3Typography.supporting)
.foregroundStyle(T3Colors.textSecondary)
.fixedSize(horizontal: false, vertical: true)
}
changeLinks(entry, repositoryURL: repositoryURL)
}
.padding(.bottom, isLast ? 0 : 22)
.frame(maxWidth: .infinity, alignment: .leading)
}
}

private func changeLinks(
_ entry: BuildChangelog.Entry,
repositoryURL: URL?
) -> some View {
HStack(spacing: 12) {
if let repositoryURL {
Link(destination: repositoryURL.appending(path: "commit/\(entry.commit)")) {
Label(entry.shortCommit, systemImage: "point.topleft.down.to.point.bottomright.curvepath")
}
} else {
Text(entry.shortCommit)
}
if let pullRequest = entry.pullRequest, let pullRequestURL = entry.pullRequestURL {
Link(destination: pullRequestURL) {
Label("PR #\(pullRequest)", systemImage: "arrow.triangle.pull")
}
}
}
.font(.caption.monospaced())
.foregroundStyle(T3Colors.accent)
}
}
36 changes: 36 additions & 0 deletions apps/swift-ios/Features/Settings/SettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,15 @@ public struct SettingsView: View {
@State private var showingT3Connect = false
@State private var removalTarget: FeatureEnvironment?
@State private var saveErrorMessage: String?
private let appVersionLabel: String
private let buildChangelog: BuildChangelog?

public init(model: FeatureRootModel) {
self.model = model
_settings = State(initialValue: model.snapshot.settings)
let info = Bundle.main.infoDictionary
appVersionLabel = SettingsAboutMetadata.appVersionLabel(info: info)
buildChangelog = BuildChangelog.embedded
}

public var body: some View {
Expand Down Expand Up @@ -322,6 +327,22 @@ public struct SettingsView: View {
VStack(spacing: 0) {
SettingsValueRow(title: "App", value: appDisplayName)
settingsDivider
SettingsValueRow(title: "Version", value: appVersionLabel)
settingsDivider
NavigationLink {
BuildChangelogView(
changelog: buildChangelog,
versionLabel: appVersionLabel
)
} label: {
SettingsNavigationRow(
title: "Build changelog",
systemImage: "clock.arrow.circlepath",
trailingSystemImage: "chevron.right"
)
}
.buttonStyle(.plain)
settingsDivider
SettingsValueRow(title: "Platform", value: "Native SwiftUI")
settingsDivider
Link(destination: URL(string: "https://github.com/pingdotgg/t3code")!) {
Expand Down Expand Up @@ -536,6 +557,21 @@ public struct SettingsView: View {
}
}

enum SettingsAboutMetadata {
static func appVersionLabel(info: [String: Any]?) -> String {
let version = nonemptyValue("CFBundleShortVersionString", info: info) ?? "?"
let build = nonemptyValue("CFBundleVersion", info: info) ?? "?"
return "\(version) (\(build))"
}

private static func nonemptyValue(_ key: String, info: [String: Any]?) -> String? {
guard let value = info?[key] as? String,
!value.isEmpty,
!value.hasPrefix("$(")
else { return nil }
return value
}
}
private struct EnvironmentStatusPresentation {
let title: String
let symbol: String
Expand Down
4 changes: 4 additions & 0 deletions apps/swift-ios/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,10 @@ overrides are `T3_SWIFT_DERIVED_DATA_PATH`, `T3_SWIFT_VERSION`, and
`T3_SWIFT_VERIFY_BUNDLE_IDENTIFIERS_ONLY=1` to verify the configuration's host
and extension bundle identifiers without a device build.

Debug installations embed an offline changelog for the commits after
`upstream/t3code/rebuild-mobile-app-swift`. Set `T3_SWIFT_CHANGELOG_BASE_REF` to
compare with another build base.

## Release checklist

1. Set a unique `MARKETING_VERSION` and a higher `CURRENT_PROJECT_VERSION`.
Expand Down
2 changes: 2 additions & 0 deletions apps/swift-ios/Resources/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@
</dict>
</dict>
</dict>
<key>T3BuildChangelog</key>
<string>$(T3_BUILD_CHANGELOG)</string>
<key>T3ConnectClerkJWTTemplate</key>
<string>$(T3CODE_CLERK_JWT_TEMPLATE)</string>
<key>T3ConnectClerkPublishableKey</key>
Expand Down
Loading
Loading