Skip to content
Open
80 changes: 80 additions & 0 deletions .github/workflows/build-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
on:
workflow_dispatch:

name: Build Test APK (Real Certs)
jobs:
build:
name: OpenBubbles Test APK
runs-on: ubuntu-latest
steps:
- name: Free Disk Space (Ubuntu)
uses: jlumbroso/free-disk-space@main
with:
tool-cache: true
android: true
dotnet: true
haskell: true
large-packages: true
swap-storage: true

- uses: actions/checkout@v4
with:
submodules: recursive

- name: Set up Rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable

- name: Set up Flutter
uses: subosito/flutter-action@v2
with:
channel: stable
flutter-version: 3.24.0

- name: Set up Java
uses: actions/setup-java@v2
with:
java-version: '21'
distribution: 'temurin'

- name: Setup Android SDK
uses: amyu/setup-android@v5

- name: Install Protobuf compiler
run: sudo apt-get install -y protobuf-compiler

- name: Set up stable debug keystore
run: |
echo "${{ secrets.DEBUG_KEYSTORE }}" | base64 -d > /home/runner/.android/debug.keystore

- name: Set up FairPlay keys from secrets
run: |
mkdir -p rustpush/certs/fairplay

cert_names=(
"4056631661436364584235346952193"
"4056631661436364584235346952194"
"4056631661436364584235346952195"
"4056631661436364584235346952196"
"4056631661436364584235346952197"
"4056631661436364584235346952198"
"4056631661436364584235346952199"
"4056631661436364584235346952200"
"4056631661436364584235346952201"
"4056631661436364584235346952208"
)

for name in "${cert_names[@]}"; do
echo "${{ secrets.FAIRPLAY_CERT }}" | base64 -d > rustpush/certs/fairplay/$name.crt
echo "${{ secrets.FAIRPLAY_KEY }}" | base64 -d > rustpush/certs/fairplay/$name.pem
done

- name: Run Build Script
run: |
flutter build apk --flavor alpha --debug --target-platform android-arm64

- uses: actions/upload-artifact@v4
with:
name: Alpha Test APK
path: build/app/outputs/flutter-apk/app-alpha-debug.apk
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:universal_io/io.dart';
import 'package:bluebubbles/src/rust/api/api.dart' as api;
import 'package:collection/collection.dart';
import 'package:bluebubbles/utils/logger/logger.dart';

class CupertinoHeader extends StatelessWidget implements PreferredSizeWidget {
const CupertinoHeader({Key? key, required this.controller});
Expand Down Expand Up @@ -456,6 +458,14 @@ class _ChatIconAndTitleState extends CustomState<_ChatIconAndTitle, void, Conver

late StreamSubscription sub2;

// --- FIND MY FRIENDS CITY/STATE ---
String? shortAddress;
bool isLoadingFindMy = false;

static final Map<String, (String?, DateTime)> _findMyCache = {};
static const _findMyCacheTtl = Duration(minutes: 5);


@override
void initState() {
super.initState();
Expand All @@ -477,6 +487,11 @@ class _ChatIconAndTitleState extends CustomState<_ChatIconAndTitle, void, Conver
title = controller.chat.getTitle();
cachedGuid = controller.chat.guid;

// --- FindMy integration ---
if (ss.settings.showLocationInChat.value) {
fetchShortAddress();
}

// run query after render has completed
if (!kIsWeb) {
updateObx(() {
Expand Down Expand Up @@ -522,6 +537,66 @@ class _ChatIconAndTitleState extends CustomState<_ChatIconAndTitle, void, Conver
}
}

Future<void> fetchShortAddress() async {
// Only fetch for 1-on-1 chats
if (controller.chat.isGroup) return;
if (pushService.state == null) return;
if (pushService.state!.icloudServices == null) return;

final handle = controller.chat.participants.firstOrNull?.address;
if (handle == null) return;

final cached = _findMyCache[handle];

if (cached != null && DateTime.now().difference(cached.$2) < _findMyCacheTtl) {
setState(() {
shortAddress = cached.$1;
isLoadingFindMy = false;
});
return;
}

setState(() => isLoadingFindMy = true);

try {
// Create a Find My Friends client using the current push state
final fmfClient = await api.makeFindMyFriends(
path: pushService.statePath,
config: pushService.state!.osConfig,
aps: pushService.state!.conn,
anisette: pushService.state!.anisette,
provider: pushService.state!.icloudServices!.tokenProvider,
);

// Fetch the current following/friends list
final following = await api.getFollowing(client: fmfClient);

// Try to match on any known handle for the friend
final friend = following.firstWhereOrNull(
(f) => f.invitationAcceptedHandles.any((h) => h.toLowerCase() == handle.toLowerCase()),
);

String? cityState;
if (friend != null && friend.lastLocation?.address != null) {
final addr = friend.lastLocation!.address!;
// E.g. "San Francisco, CA" or fallback to "Country" if stateCode is missing
if (addr.locality != null && (addr.stateCode != null || addr.countryCode != null)) {
cityState = "${addr.locality}, ${addr.stateCode ?? addr.countryCode}";
}
}

_findMyCache[handle] = (cityState, DateTime.now());

setState(() {
shortAddress = cityState;
isLoadingFindMy = false;
});
} catch (e) {
Logger.error("Failed to fetch FindMy location in convo view", error: e);
setState(() => isLoadingFindMy = false);
}
}

@override
void dispose() {
sub.cancel();
Expand All @@ -536,15 +611,16 @@ class _ChatIconAndTitleState extends CustomState<_ChatIconAndTitle, void, Conver
if (hideInfo) {
_title = controller.chat.participants.length > 1 ? "Group Chat" : controller.chat.participants[0].fakeName;
}
final hasLocationRow = isLoadingFindMy || (shortAddress != null && shortAddress!.isNotEmpty);
final children = [
IgnorePointer(
ignoring: true,
child: ContactAvatarGroupWidget(
chat: controller.chat,
size: 54,
size: hasLocationRow ? 40 : 54,
),
),
const SizedBox(height: 5, width: 5),
SizedBox(height: hasLocationRow ? 2 : 5, width: 5),
Row(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, children: [
ConstrainedBox(
constraints: BoxConstraints(
Expand All @@ -569,6 +645,23 @@ class _ChatIconAndTitleState extends CustomState<_ChatIconAndTitle, void, Conver
color: context.theme.colorScheme.outline,
),
]),
if (isLoadingFindMy)
Padding(
padding: const EdgeInsets.only(top: 2.0, left: 6.0),
child: SizedBox(
height: 12,
width: 12,
child: CircularProgressIndicator(strokeWidth: 2),
),
)
else if (shortAddress != null && shortAddress!.isNotEmpty)
Padding(
padding: const EdgeInsets.only(top: 2.0, left: 6.0),
child: Text(
shortAddress!,
style: context.theme.textTheme.bodySmall?.copyWith(color: context.theme.colorScheme.outline),
),
),
];

if (context.orientation == Orientation.landscape && Platform.isAndroid) {
Expand Down
Loading