Compare commits
2 Commits
release/an
...
Kordophone
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
40fb964cb3 | ||
|
|
d814c2e4f6 |
@@ -1,63 +0,0 @@
|
|||||||
name: Android Release
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- 'release/android/*'
|
|
||||||
|
|
||||||
env:
|
|
||||||
ANDROID_SDK_ROOT: ${{ gitea.workspace }}/android-sdk
|
|
||||||
ANDROID_HOME: ${{ gitea.workspace }}/android-sdk
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build-android-release:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
# Gitea's default act_runner labels map ubuntu-latest to node:16-bullseye,
|
|
||||||
# so keep the GitHub-hosted actions on their Node 16-compatible v3 line.
|
|
||||||
- name: Install system dependencies
|
|
||||||
run: |
|
|
||||||
set -eu
|
|
||||||
apt-get update
|
|
||||||
apt-get install -y ca-certificates git openjdk-17-jdk unzip wget
|
|
||||||
|
|
||||||
- name: Check out repository code
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Install Android SDK components
|
|
||||||
run: |
|
|
||||||
set -eu
|
|
||||||
|
|
||||||
wget -q https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip -O /tmp/android-commandlinetools.zip
|
|
||||||
|
|
||||||
rm -rf "$ANDROID_SDK_ROOT"
|
|
||||||
mkdir -p "$ANDROID_SDK_ROOT/cmdline-tools"
|
|
||||||
|
|
||||||
unzip -q /tmp/android-commandlinetools.zip -d /tmp/android-commandlinetools
|
|
||||||
mv /tmp/android-commandlinetools/cmdline-tools "$ANDROID_SDK_ROOT/cmdline-tools/latest"
|
|
||||||
|
|
||||||
# sdkmanager exits successfully once it has consumed all input, which
|
|
||||||
# causes `yes` to receive SIGPIPE and return 141 under `pipefail`.
|
|
||||||
set +o pipefail
|
|
||||||
yes | "$ANDROID_SDK_ROOT/cmdline-tools/latest/bin/sdkmanager" --sdk_root="$ANDROID_SDK_ROOT" --licenses
|
|
||||||
set -o pipefail
|
|
||||||
|
|
||||||
"$ANDROID_SDK_ROOT/cmdline-tools/latest/bin/sdkmanager" --sdk_root="$ANDROID_SDK_ROOT" \
|
|
||||||
"platform-tools" \
|
|
||||||
"build-tools;33.0.1" \
|
|
||||||
"platforms;android-33"
|
|
||||||
|
|
||||||
- name: Build Android release APKs
|
|
||||||
working-directory: android
|
|
||||||
run: ./gradlew assembleRelease
|
|
||||||
|
|
||||||
- name: Upload release artifacts
|
|
||||||
uses: actions/upload-artifact@v3
|
|
||||||
with:
|
|
||||||
name: kordophone-android-release
|
|
||||||
path: |
|
|
||||||
android/app/build/outputs/apk/release/*.apk
|
|
||||||
android/app/build/outputs/apk/release/output-metadata.json
|
|
||||||
if-no-files-found: error
|
|
||||||
retention-days: 90
|
|
||||||
2
.gitignore
vendored
@@ -1,2 +0,0 @@
|
|||||||
ext/
|
|
||||||
target/
|
|
||||||
2
.gitmodules
vendored
@@ -1,3 +1,3 @@
|
|||||||
[submodule "CocoaHTTPServer"]
|
[submodule "CocoaHTTPServer"]
|
||||||
path = server/CocoaHTTPServer
|
path = CocoaHTTPServer
|
||||||
url = https://github.com/robbiehanson/CocoaHTTPServer.git
|
url = https://github.com/robbiehanson/CocoaHTTPServer.git
|
||||||
|
|||||||
@@ -1754,13 +1754,10 @@ __attribute__((visibility("default"))) @interface IMService : NSObject { }
|
|||||||
@property(nonatomic) BOOL chatInScrutinyMode; // @synthesize chatInScrutinyMode=_chatInScrutinyMode;
|
@property(nonatomic) BOOL chatInScrutinyMode; // @synthesize chatInScrutinyMode=_chatInScrutinyMode;
|
||||||
@property(readonly, nonatomic) NSArray *messageEditChatItems; // @synthesize messageEditChatItems=_messageEditChatItems;
|
@property(readonly, nonatomic) NSArray *messageEditChatItems; // @synthesize messageEditChatItems=_messageEditChatItems;
|
||||||
@property(retain, nonatomic, setter=_setVisibleAssociatedMessageChatItems:) NSArray *visibleAssociatedMessageChatItems; // @synthesize visibleAssociatedMessageChatItems=_visibleAssociatedMessageChatItems;
|
@property(retain, nonatomic, setter=_setVisibleAssociatedMessageChatItems:) NSArray *visibleAssociatedMessageChatItems; // @synthesize visibleAssociatedMessageChatItems=_visibleAssociatedMessageChatItems;
|
||||||
|
@property(nonatomic) struct _NSRange messagePartRange; // @synthesize messagePartRange=_messagePartRange;
|
||||||
@property(nonatomic) long long index; // @synthesize index=_index;
|
@property(nonatomic) long long index; // @synthesize index=_index;
|
||||||
@property(readonly, copy, nonatomic) NSAttributedString *text; // @synthesize text=_text;
|
@property(readonly, copy, nonatomic) NSAttributedString *text; // @synthesize text=_text;
|
||||||
|
|
||||||
@property (nonatomic, copy, readonly) NSString *threadIdentifier;
|
|
||||||
@property(nonatomic) struct _NSRange messagePartRange; // @synthesize messagePartRange=_messagePartRange;
|
|
||||||
|
|
||||||
|
|
||||||
- (BOOL)canSendMessageAcknowledgment;
|
- (BOOL)canSendMessageAcknowledgment;
|
||||||
- (void)_setMessageEditChatItems:(id)arg1;
|
- (void)_setMessageEditChatItems:(id)arg1;
|
||||||
- (id)_initWithItem:(id)arg1 text:(id)arg2 index:(long long)arg3 messagePartRange:(struct _NSRange)arg4 visibleAssociatedMessageChatItems:(id)arg5;
|
- (id)_initWithItem:(id)arg1 text:(id)arg2 index:(long long)arg3 messagePartRange:(struct _NSRange)arg4 visibleAssociatedMessageChatItems:(id)arg5;
|
||||||
@@ -3460,7 +3457,6 @@ __attribute__((visibility("default"))) @interface IMService : NSObject { }
|
|||||||
- (id)loadUnreadMessagesWithLimit:(unsigned long long)arg1 fallbackToMessagesUpToGUID:(id)arg2;
|
- (id)loadUnreadMessagesWithLimit:(unsigned long long)arg1 fallbackToMessagesUpToGUID:(id)arg2;
|
||||||
- (id)loadFrequentRepliesLimit:(unsigned long long)arg1 loadImmediately:(BOOL)arg2;
|
- (id)loadFrequentRepliesLimit:(unsigned long long)arg1 loadImmediately:(BOOL)arg2;
|
||||||
- (id)loadMessagesBeforeAndAfterGUID:(id)arg1 numberOfMessagesToLoadBeforeGUID:(unsigned long long)arg2 numberOfMessagesToLoadAfterGUID:(unsigned long long)arg3 loadImmediately:(BOOL)arg4;
|
- (id)loadMessagesBeforeAndAfterGUID:(id)arg1 numberOfMessagesToLoadBeforeGUID:(unsigned long long)arg2 numberOfMessagesToLoadAfterGUID:(unsigned long long)arg3 loadImmediately:(BOOL)arg4;
|
||||||
- (id)loadMessagesBeforeAndAfterGUID:(id)arg1 numberOfMessagesToLoadBeforeGUID:(unsigned long long)arg2 numberOfMessagesToLoadAfterGUID:(unsigned long long)arg3 loadImmediately:(BOOL)arg4 threadIdentifier:(id)tid;
|
|
||||||
- (id)loadMessagesUpToGUID:(id)arg1 date:(id)arg2 limit:(unsigned long long)arg3 loadImmediately:(BOOL)arg4;
|
- (id)loadMessagesUpToGUID:(id)arg1 date:(id)arg2 limit:(unsigned long long)arg3 loadImmediately:(BOOL)arg4;
|
||||||
- (id)loadMessagesBeforeDate:(id)arg1 limit:(unsigned long long)arg2 loadImmediately:(BOOL)arg3;
|
- (id)loadMessagesBeforeDate:(id)arg1 limit:(unsigned long long)arg2 loadImmediately:(BOOL)arg3;
|
||||||
- (id)loadMessagesBeforeDate:(id)arg1 limit:(unsigned long long)arg2;
|
- (id)loadMessagesBeforeDate:(id)arg1 limit:(unsigned long long)arg2;
|
||||||
@@ -3683,68 +3679,6 @@ __attribute__((visibility("default"))) @interface IMService : NSObject { }
|
|||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
typedef NS_ENUM(NSInteger, IMMessageDescriptionType) {
|
|
||||||
IMMessageDescriptionAccessibility,
|
|
||||||
IMMessageDescriptionAcknowledgement,
|
|
||||||
IMMessageDescriptionConversationList,
|
|
||||||
IMMessageDescriptionNotification,
|
|
||||||
IMMessageDescriptionSiri,
|
|
||||||
IMMessageDescriptionSPI,
|
|
||||||
};
|
|
||||||
|
|
||||||
typedef NS_ENUM(int64_t, IMAssociatedMessageType) {
|
|
||||||
IMAssociatedMessageTypeUnspecified = 0,
|
|
||||||
|
|
||||||
IMAssociatedMessageTypeBreadcrumbUnconsumed = 2,
|
|
||||||
IMAssociatedMessageTypeBreadcrumbConsumed = 3,
|
|
||||||
IMAssociatedMessageTypeSticker = 1000,
|
|
||||||
IMAssociatedMessageTypeEmojiSticker = 1001,
|
|
||||||
|
|
||||||
IMAssociatedMessageTypeAcknowledgmentHeart = 2000,
|
|
||||||
IMAssociatedMessageTypeAcknowledgmentThumbsUp = 2001,
|
|
||||||
IMAssociatedMessageTypeAcknowledgmentThumbsDown = 2002,
|
|
||||||
IMAssociatedMessageTypeAcknowledgmentHa = 2003,
|
|
||||||
IMAssociatedMessageTypeAcknowledgmentExclamation = 2004,
|
|
||||||
IMAssociatedMessageTypeAcknowledgmentQuestionMark = 2005,
|
|
||||||
|
|
||||||
IMAssociatedMessageTypeAcknowledgmentHeartRemoved = 3000,
|
|
||||||
IMAssociatedMessageTypeAcknowledgmentThumbsUpRemoved = 3001,
|
|
||||||
IMAssociatedMessageTypeAcknowledgmentThumbsDownRemoved = 3002,
|
|
||||||
IMAssociatedMessageTypeAcknowledgmentHaRemoved = 3003,
|
|
||||||
IMAssociatedMessageTypeAcknowledgmentExclamationRemoved = 3004,
|
|
||||||
IMAssociatedMessageTypeAcknowledgmentQuestionMarkRemoved = 3005,
|
|
||||||
};
|
|
||||||
|
|
||||||
extern NSString * const IMMessageSummaryInfoSummary;
|
|
||||||
extern NSString * const IMMessageSummaryInfoContentType;
|
|
||||||
extern NSString * const IMMessageSummaryInfoPluginBundleID;
|
|
||||||
extern NSString * const IMMessageSummaryInfoPluginDisplayName;
|
|
||||||
extern NSString * const IMMessageSummaryInfoAssociatedBalloonBundleID;
|
|
||||||
extern NSString * const IMMessageSummaryInfoSourceApplicationID;
|
|
||||||
extern NSString * const IMMessageSummaryInfoUpdatedDateWithServerTime;
|
|
||||||
extern NSString * const IMMessageSummaryInfoHasBeenRetried;
|
|
||||||
extern NSString * const IMMessageSummaryInfoTapbackRepresentationKey;
|
|
||||||
extern NSString * const IMMessageSummaryInfoStickerRepositioningKey;
|
|
||||||
extern NSString * const IMMessageSummaryInfoTranscriptSharingMessageTypeKey;
|
|
||||||
extern NSString * const IMMessageSummaryInfoTranscriptSharingMessageFirstItemKey;
|
|
||||||
extern NSString * const IMMessageSummaryInfoEditedPartIndexes;
|
|
||||||
extern NSString * const IMMessageSummaryInfoRetractedPartIndexes;
|
|
||||||
extern NSString * const IMMessageSummaryInfoOriginalTextRangesByPartIndex;
|
|
||||||
extern NSString * const IMMessageSummaryInfoOriginalTextRangeLocationSubKey;
|
|
||||||
extern NSString * const IMMessageSummaryInfoOriginalTextRangeLengthSubKey;
|
|
||||||
extern NSString * const IMMessageSummaryInfoEditDeliveryFailedForPartIndexes;
|
|
||||||
extern NSString * const IMMessageSummaryInfoRetractDeliveryFailedForPartIndexes;
|
|
||||||
extern NSString * const IMMessageSummaryInfoEditUnsupportedByHandleIDs;
|
|
||||||
extern NSString * const IMMessageSummaryInfoTranslatedText;
|
|
||||||
extern NSString * const IMMessageSummaryInfoDetectedLanguage;
|
|
||||||
extern NSString * const IMMessageSummaryInfoCMMState;
|
|
||||||
extern NSString * const IMMessageSummaryInfoCMMAssetOffset;
|
|
||||||
extern NSString * const IMMessageItemErrorDomain;
|
|
||||||
extern NSString * const IMMessageSentDistributedNotification;
|
|
||||||
extern NSString * const IMMessageSentDistributedNotificationUserInfoMessageGUIDKey;
|
|
||||||
extern NSString * const IMMessageSentDistributedNotificationUserInfoSucessKey;
|
|
||||||
|
|
||||||
|
|
||||||
@interface IMMessage : NSObject <NSCopying>
|
@interface IMMessage : NSObject <NSCopying>
|
||||||
{
|
{
|
||||||
IMHandle *_sender;
|
IMHandle *_sender;
|
||||||
@@ -3800,22 +3734,6 @@ extern NSString * const IMMessageSentDistributedNotificationUserInfoSucessKey;
|
|||||||
+ (id)breadcrumbMessageWithText:(id)arg1 associatedMessageGUID:(id)arg2 balloonBundleID:(id)arg3 fileTransferGUIDs:(id)arg4 payloadData:(id)arg5;
|
+ (id)breadcrumbMessageWithText:(id)arg1 associatedMessageGUID:(id)arg2 balloonBundleID:(id)arg3 fileTransferGUIDs:(id)arg4 payloadData:(id)arg5;
|
||||||
+ (id)editedMessageWithOriginalMessage:(id)arg1 originalPrefixedGUID:(id)arg2 newBody:(id)arg3;
|
+ (id)editedMessageWithOriginalMessage:(id)arg1 originalPrefixedGUID:(id)arg2 newBody:(id)arg3;
|
||||||
+ (id)instantMessageWithAssociatedMessageContent:(id)arg1 flags:(unsigned long long)arg2 associatedMessageGUID:(id)arg3 associatedMessageType:(long long)arg4 associatedMessageRange:(struct _NSRange)arg5 messageSummaryInfo:(id)arg6;
|
+ (id)instantMessageWithAssociatedMessageContent:(id)arg1 flags:(unsigned long long)arg2 associatedMessageGUID:(id)arg3 associatedMessageType:(long long)arg4 associatedMessageRange:(struct _NSRange)arg5 messageSummaryInfo:(id)arg6;
|
||||||
|
|
||||||
+ (instancetype)instantMessageWithAssociatedMessageContent:(NSAttributedString *) content
|
|
||||||
flags:(IMMessageFlags)flags
|
|
||||||
associatedMessageGUID:(NSString*) associatedMessageGUID
|
|
||||||
associatedMessageType:(IMAssociatedMessageType) associatedMessageType
|
|
||||||
associatedMessageRange:(NSRange) associatedMessageRange
|
|
||||||
messageSummaryInfo:(NSDictionary*) messageSummaryInfo
|
|
||||||
threadIdentifier:(NSString *)threadIdentifier;
|
|
||||||
|
|
||||||
+ (instancetype)breadcrumbMessageWithText:(NSAttributedString *)text
|
|
||||||
associatedMessageGUID:(NSString *)messageGUID
|
|
||||||
balloonBundleID:(NSString *)balloonBundleID
|
|
||||||
fileTransferGUIDs:(NSArray<NSString *> *)fileTransferGUIDs
|
|
||||||
payloadData:(NSData *)payloadData
|
|
||||||
threadIdentifier:(NSString *)threadIdentifier;
|
|
||||||
|
|
||||||
@property(nonatomic) unsigned long long sortID; // @synthesize sortID=_sortID;
|
@property(nonatomic) unsigned long long sortID; // @synthesize sortID=_sortID;
|
||||||
@property(nonatomic) BOOL isSOS; // @synthesize isSOS=_isSOS;
|
@property(nonatomic) BOOL isSOS; // @synthesize isSOS=_isSOS;
|
||||||
@property(retain, nonatomic) NSString *notificationIDSTokenURI; // @synthesize notificationIDSTokenURI=_notificationIDSTokenURI;
|
@property(retain, nonatomic) NSString *notificationIDSTokenURI; // @synthesize notificationIDSTokenURI=_notificationIDSTokenURI;
|
||||||
@@ -3885,8 +3803,8 @@ extern NSString * const IMMessageSentDistributedNotificationUserInfoSucessKey;
|
|||||||
- (id)_initWithSender:(id)arg1 time:(id)arg2 timeRead:(id)arg3 timeDelivered:(id)arg4 timePlayed:(id)arg5 plainText:(id)arg6 text:(id)arg7 messageSubject:(id)arg8 fileTransferGUIDs:(id)arg9 flags:(unsigned long long)arg10 error:(id)arg11 guid:(id)arg12 messageID:(long long)arg13 subject:(id)arg14 balloonBundleID:(id)arg15 payloadData:(id)arg16 expressiveSendStyleID:(id)arg17 timeExpressiveSendPlayed:(id)arg18 associatedMessageGUID:(id)arg19 associatedMessageType:(long long)arg20 associatedMessageRange:(struct _NSRange)arg21 messageSummaryInfo:(id)arg22;
|
- (id)_initWithSender:(id)arg1 time:(id)arg2 timeRead:(id)arg3 timeDelivered:(id)arg4 timePlayed:(id)arg5 plainText:(id)arg6 text:(id)arg7 messageSubject:(id)arg8 fileTransferGUIDs:(id)arg9 flags:(unsigned long long)arg10 error:(id)arg11 guid:(id)arg12 messageID:(long long)arg13 subject:(id)arg14 balloonBundleID:(id)arg15 payloadData:(id)arg16 expressiveSendStyleID:(id)arg17 timeExpressiveSendPlayed:(id)arg18 associatedMessageGUID:(id)arg19 associatedMessageType:(long long)arg20 associatedMessageRange:(struct _NSRange)arg21 messageSummaryInfo:(id)arg22;
|
||||||
- (id)_copyWithFlags:(unsigned long long)arg1;
|
- (id)_copyWithFlags:(unsigned long long)arg1;
|
||||||
- (id)copyWithZone:(struct _NSZone *)arg1;
|
- (id)copyWithZone:(struct _NSZone *)arg1;
|
||||||
- (id)descriptionForPurpose:(IMMessageDescriptionType)arg1 inChat:(id)arg2;
|
- (id)descriptionForPurpose:(long long)arg1 inChat:(id)arg2;
|
||||||
- (id)descriptionForPurpose:(IMMessageDescriptionType)arg1;
|
- (id)descriptionForPurpose:(long long)arg1;
|
||||||
- (void)_ovverrideGUIDForTest:(id)arg1;
|
- (void)_ovverrideGUIDForTest:(id)arg1;
|
||||||
@property(readonly, nonatomic) BOOL isAssociatedMessage;
|
@property(readonly, nonatomic) BOOL isAssociatedMessage;
|
||||||
- (id)initWithSender:(id)arg1 time:(id)arg2 text:(id)arg3 messageSubject:(id)arg4 fileTransferGUIDs:(id)arg5 flags:(unsigned long long)arg6 error:(id)arg7 guid:(id)arg8 subject:(id)arg9 associatedMessageGUID:(id)arg10 associatedMessageType:(long long)arg11 associatedMessageRange:(struct _NSRange)arg12 associatedMessageInfo:(id)arg13;
|
- (id)initWithSender:(id)arg1 time:(id)arg2 text:(id)arg3 messageSubject:(id)arg4 fileTransferGUIDs:(id)arg5 flags:(unsigned long long)arg6 error:(id)arg7 guid:(id)arg8 subject:(id)arg9 associatedMessageGUID:(id)arg10 associatedMessageType:(long long)arg11 associatedMessageRange:(struct _NSRange)arg12 associatedMessageInfo:(id)arg13;
|
||||||
@@ -4161,31 +4079,39 @@ extern NSString * const IMMessageSentDistributedNotificationUserInfoSucessKey;
|
|||||||
@property(readonly) unsigned long long hash;
|
@property(readonly) unsigned long long hash;
|
||||||
@property(readonly) Class superclass;
|
@property(readonly) Class superclass;
|
||||||
|
|
||||||
- (IMChat *)chatWithHandle:(IMHandle *)handle;
|
@end
|
||||||
- (IMChat *)chatWithHandle:(IMHandle *)handle lastAddressedHandle:( NSString *)lastAddressedHandle lastAddressedSIMID:( NSString *)lastAddressedSIMID;
|
|
||||||
- (IMChat *)chatWithHandles:(NSArray<IMHandle *> *)handles;
|
|
||||||
- (IMChat *)chatWithHandles:(NSArray<IMHandle *> *)handles lastAddressedHandle:( NSString *)lastAddressedHandle lastAddressedSIMID:( NSString *)lastAddressedSIMID;
|
|
||||||
- (IMChat *)chatWithHandles:(NSArray<IMHandle *> *)handles displayName:( NSString *)displayName joinedChatsOnly:(BOOL)joinedChatsOnly;
|
|
||||||
- (IMChat *)chatWithHandles:(NSArray<IMHandle *> *)handles displayName:( NSString *)displayName joinedChatsOnly:(BOOL)joinedChatsOnly lastAddressedHandle:( NSString *)lastAddressedHandle lastAddressedSIMID:( NSString *)lastAddressedSIMID;
|
|
||||||
|
|
||||||
|
// Monterey
|
||||||
|
#if 0
|
||||||
|
@interface IMChatRegistry ()
|
||||||
|
- (IMChat *)chatWithHandle:(IMHandle *)handle;
|
||||||
|
- (IMChat *)chatWithHandle:(IMHandle *)handle lastAddressedHandle:(NSString *)lastAddressedHandle lastAddressedSIMID:(NSString *)lastAddressedSIMID;
|
||||||
|
- (IMChat *)chatWithHandles:(NSArray<IMHandle *> *)handles;
|
||||||
|
- (IMChat *)chatWithHandles:(NSArray<IMHandle *> *)handles lastAddressedHandle:(NSString *)lastAddressedHandle lastAddressedSIMID:(NSString *)lastAddressedSIMID;
|
||||||
|
- (IMChat *)chatWithHandles:(NSArray<IMHandle *> *)handles displayName:(NSString *)displayName joinedChatsOnly:(BOOL)joinedChatsOnly;
|
||||||
|
- (IMChat *)chatWithHandles:(NSArray<IMHandle *> *)handles displayName:(NSString *)displayName joinedChatsOnly:(BOOL)joinedChatsOnly lastAddressedHandle:(NSString *)lastAddressedHandle lastAddressedSIMID:(NSString *)lastAddressedSIMID;
|
||||||
- (NSArray<NSString *> *)allGUIDsForChat:(IMChat *)chat;
|
- (NSArray<NSString *> *)allGUIDsForChat:(IMChat *)chat;
|
||||||
- ( IMChat *)existingChatWithHandle:(IMHandle *)handle;
|
|
||||||
- ( IMChat *)existingChatWithHandle:(IMHandle *)handle allowAlternativeService:(BOOL)allowAlternativeService;
|
- (IMChat *)existingChatWithHandle:(IMHandle *)handle;
|
||||||
- ( IMChat *)existingChatWithHandles:(NSArray<IMHandle *> *)handles;
|
- (IMChat *)existingChatWithHandle:(IMHandle *)handle allowAlternativeService:(BOOL)allowAlternativeService;
|
||||||
- ( IMChat *)existingChatWithHandles:(NSArray<IMHandle *> *)handles allowAlternativeService:(BOOL)allowAlternativeService;
|
- (IMChat *)existingChatWithHandles:(NSArray<IMHandle *> *)handles;
|
||||||
- ( IMChat *)existingChatWithHandles:(NSArray<IMHandle *> *)handles allowAlternativeService:(BOOL)allowAlternativeService groupID:(NSString *)groupID;
|
- (IMChat *)existingChatWithHandles:(NSArray<IMHandle *> *)handles allowAlternativeService:(BOOL)allowAlternativeService;
|
||||||
- ( IMChat *)existingChatWithHandles:(NSArray *)handles allowAlternativeService:(BOOL)allowAlternativeService groupID:( NSString *)groupID displayName:( NSString *)displayName joinedChatsOnly:(BOOL)joinedChatsOnly;
|
|
||||||
// - ( IMChat *)existingChatWithGUID:(NSString *)guid;
|
- (IMChat *)existingChatWithHandles:(NSArray<IMHandle *> *)handles allowAlternativeService:(BOOL)allowAlternativeService groupID:(NSString *)groupID;
|
||||||
- ( IMChat *)existingChatWithPinningIdentifier:(NSString *)pinningIdentifier;
|
|
||||||
- ( IMChat *)existingChatWithDeviceIndependentID:(NSString *)deviceIndependentID;
|
- (IMChat *)existingChatWithHandles:(NSArray *)handles allowAlternativeService:(BOOL)allowAlternativeService groupID:(NSString *)groupID displayName:(NSString *)displayName joinedChatsOnly:(BOOL)joinedChatsOnly;
|
||||||
// - ( IMChat *)existingChatWithChatIdentifier:(NSString *)identifier;
|
|
||||||
- ( IMChat *)existingChatWithPersonID:(NSString *)personID;
|
- (IMChat *)existingChatWithPinningIdentifier:(NSString *)pinningIdentifier;
|
||||||
// - ( IMChat *)existingChatWithGroupID:(NSString *)groupID;
|
- (IMChat *)existingChatWithDeviceIndependentID:(NSString *)deviceIndependentID;
|
||||||
// - ( IMChat *)existingChatWithDisplayName:(NSString *)displayName;
|
|
||||||
- ( IMChat *)existingChatWithAddresses:(NSArray<NSString *> *)addresses allowAlternativeService:(BOOL)allowAlternativeService bestHandles:(NSArray<IMHandle *> * __autoreleasing*)outBestHandles;
|
- (IMChat *)existingChatWithPersonID:(NSString *)personID;
|
||||||
- ( IMChat *)existingChatWithContacts:(NSSet /* <CNContact *> */ *)contacts bestHandles:(NSArray<IMHandle *> * __autoreleasing * )outBestHandles;
|
- (IMChat *)existingChatWithDisplayName:(NSString *)displayName;
|
||||||
|
- (IMChat *)existingChatWithAddresses:(NSArray<NSString *> *)addresses allowAlternativeService:(BOOL)allowAlternativeService bestHandles:(NSArray<IMHandle *> **)outBestHandles;
|
||||||
|
- (IMChat *)existingChatWithContacts:(NSSet *)contacts bestHandles:(NSArray<IMHandle *> **)outBestHandles;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
@interface IMSimulatedChat : IMChat // <IMSimulatedChatDelegate, IMSimulatedDaemonListener>
|
@interface IMSimulatedChat : IMChat // <IMSimulatedChatDelegate, IMSimulatedDaemonListener>
|
||||||
{
|
{
|
||||||
@@ -5080,66 +5006,3 @@ extern NSString * const IMMessageSentDistributedNotificationUserInfoSucessKey;
|
|||||||
- (struct __CFArray *)copyDDResultArrayByScanningStringForURLs;
|
- (struct __CFArray *)copyDDResultArrayByScanningStringForURLs;
|
||||||
@end
|
@end
|
||||||
|
|
||||||
NS_ASSUME_NONNULL_BEGIN
|
|
||||||
|
|
||||||
typedef NS_ENUM(NSInteger, FZIDType) {
|
|
||||||
FZUnknownIDType = -1,
|
|
||||||
FZPhoneNumberBasedID = 0,
|
|
||||||
FZEmailBasedID = 1,
|
|
||||||
FZDSBasedID = 2,
|
|
||||||
FZBusinessBasedID = 3,
|
|
||||||
FZTemporaryBasedID = 4,
|
|
||||||
FZPseudonymBasedID = 5,
|
|
||||||
FZHardwareBasedID = 6,
|
|
||||||
FZSIPBasedID = 7,
|
|
||||||
};
|
|
||||||
|
|
||||||
@interface NSString (FezAdditions)
|
|
||||||
|
|
||||||
- (FZIDType) _FZIDType;
|
|
||||||
- (FZIDType) _FZBestGuessFZIDType;
|
|
||||||
|
|
||||||
- (NSString *) _IDFromFZIDType:(FZIDType)IDType;
|
|
||||||
- (NSString *) _stripFZIDPrefix;
|
|
||||||
|
|
||||||
- (NSString *) _URIFromFZIDType:(FZIDType)IDType;
|
|
||||||
- (NSString *) _bestGuessURI;
|
|
||||||
|
|
||||||
@end
|
|
||||||
|
|
||||||
extern BOOL IMStringIsEmail( NSString * string );
|
|
||||||
extern BOOL IMStringIsPhoneNumber( NSString * string );
|
|
||||||
extern BOOL IMStringIsBusinessID( NSString * string );
|
|
||||||
extern BOOL IMStringIsTemporaryID( NSString * string );
|
|
||||||
extern BOOL IMStringIsPseudonymID( NSString * string );
|
|
||||||
extern BOOL IMStringIsHardwareID( NSString * string );
|
|
||||||
extern BOOL IMStringIsSIPID( NSString * string );
|
|
||||||
|
|
||||||
enum {
|
|
||||||
IMChatServiceForSendingAvailabilityErrorNone = 0,
|
|
||||||
IMChatServiceForSendingAvailabilityErrorTooManyRecipients = 1,
|
|
||||||
IMChatServiceForSendingAvailabilityErrorIMessageRequired = 2,
|
|
||||||
IMChatServiceForSendingAvailabilityErrorMMSRequired = 3,
|
|
||||||
IMChatServiceForSendingAvailabilityErrorNoAvailableServices = 4,
|
|
||||||
IMChatServiceForSendingAvailabilityErrorSpamFiltered = 5,
|
|
||||||
};
|
|
||||||
typedef int8_t IMChatServiceForSendingAvailabilityError;
|
|
||||||
|
|
||||||
extern void IMChatCalculateServiceForSendingNewComposeMaybeForce(NSArray<NSString *> *canonicalIDSAddresses,
|
|
||||||
NSString* _Nullable senderLastAddressedHandle,
|
|
||||||
NSString* _Nullable senderLastAddressedSIMID,
|
|
||||||
BOOL forceMMS,
|
|
||||||
BOOL hasEmailRecipients,
|
|
||||||
BOOL lastSentMessageWasNotDelivered,
|
|
||||||
BOOL conversationWasDowngraded,
|
|
||||||
BOOL hasConversationHistory,
|
|
||||||
IMService * _Nullable previousService,
|
|
||||||
void(^ _Nonnull completion)(BOOL allAddressesiMessageCapable,
|
|
||||||
NSDictionary * _Nullable perRecipientAvailability,
|
|
||||||
BOOL checkedServer,
|
|
||||||
IMChatServiceForSendingAvailabilityError error));
|
|
||||||
|
|
||||||
extern NSString *IMCopyIDForPhoneNumber(NSString *phoneNumber, NSString *_Nullable countryCode, BOOL useNetworkCountryCode) NS_RETURNS_RETAINED;
|
|
||||||
extern NSString *IMCopyIDForEmailAddress(NSString *emailAddress) NS_RETURNS_RETAINED;
|
|
||||||
|
|
||||||
NS_ASSUME_NONNULL_END
|
|
||||||
@@ -1898,3 +1898,29 @@ typedef void (^CDUnknownBlockType)(void); // return type and parameters are unkn
|
|||||||
- (void)differencesFromArray:(id)arg1 removedIndexes:(id *)arg2 insertedIndexes:(id *)arg3;
|
- (void)differencesFromArray:(id)arg1 removedIndexes:(id *)arg2 insertedIndexes:(id *)arg3;
|
||||||
@end
|
@end
|
||||||
|
|
||||||
|
extern BOOL IMStringIsEmail (NSString *string);
|
||||||
|
|
||||||
|
extern NSString* IMStripFormattingFromAddress (NSString *address);
|
||||||
|
|
||||||
|
typedef void(^IMChatCalculateServiceForSendingCompletionBlock) (BOOL allAddressesiMessageCapable,
|
||||||
|
NSDictionary *availabilityPerRecipient,
|
||||||
|
BOOL checkedServer,
|
||||||
|
NSError *error);
|
||||||
|
|
||||||
|
extern void IMChatCalculateServiceForSendingNewComposeMaybeForce (NSArray *addresses,
|
||||||
|
NSString *senderLastAddressedHandle,
|
||||||
|
NSString *senderLastAddressedSIMID,
|
||||||
|
BOOL forceMMS,
|
||||||
|
BOOL hasEmailRecipients,
|
||||||
|
BOOL lastSentMessageWasNotDelivered,
|
||||||
|
BOOL conversationWasDowngraded,
|
||||||
|
BOOL hasConversationHistory,
|
||||||
|
IMService *previousService,
|
||||||
|
IMChatCalculateServiceForSendingCompletionBlock completion);
|
||||||
|
|
||||||
|
|
||||||
|
// IDS
|
||||||
|
|
||||||
|
extern NSString *IDSCopyIDForPhoneNumber(NSString *phoneNumber);
|
||||||
|
extern NSString *IDSCopyIDForEmailAddress(NSString *emailAddress);
|
||||||
|
|
||||||
@@ -18,18 +18,12 @@ struct IMFileLocation_t {
|
|||||||
int _field5;
|
int _field5;
|
||||||
};
|
};
|
||||||
|
|
||||||
typedef struct IMPreviewConstraints {
|
struct IMPreviewConstraints {
|
||||||
CGFloat maxPxWidth;
|
double _field1;
|
||||||
CGSize minThumbnailPxSize;
|
struct CGSize _field2;
|
||||||
CGFloat scale;
|
double _field3;
|
||||||
BOOL isSticker;
|
char _field4;
|
||||||
BOOL generateMetadata;
|
};
|
||||||
} IMPreviewConstraints;
|
|
||||||
|
|
||||||
extern IMPreviewConstraints IMPreviewConstraintsFromDictionary(NSDictionary *dictionary);
|
|
||||||
extern NSDictionary *IMPreviewConstraintsDictionaryFromConstraint(IMPreviewConstraints constraint);
|
|
||||||
extern BOOL IMPreviewConstraintsEqualToConstraints(IMPreviewConstraints constraints1, IMPreviewConstraints constraints2);
|
|
||||||
extern IMPreviewConstraints IMPreviewConstraintsZero(void);
|
|
||||||
|
|
||||||
struct _TidyDoc {
|
struct _TidyDoc {
|
||||||
int _field1;
|
int _field1;
|
||||||
@@ -161,8 +155,6 @@ struct __va_list_tag {
|
|||||||
- (void)setObject:(id)arg1 forKey:(id)arg2;
|
- (void)setObject:(id)arg1 forKey:(id)arg2;
|
||||||
@end
|
@end
|
||||||
|
|
||||||
extern NSURL* IMAttachmentPreviewFileURL(NSURL *attachmentURL, NSString *extension, BOOL generateIntermediaryDirectories);
|
|
||||||
|
|
||||||
@protocol IMPreviewGeneratorProtocol
|
@protocol IMPreviewGeneratorProtocol
|
||||||
+ (BOOL)shouldShadePreview;
|
+ (BOOL)shouldShadePreview;
|
||||||
+ (BOOL)shouldScaleUpPreview;
|
+ (BOOL)shouldScaleUpPreview;
|
||||||
@@ -753,19 +745,6 @@ extern NSURL* IMAttachmentPreviewFileURL(NSURL *attachmentURL, NSString *extensi
|
|||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@interface IMImageUtilities : NSObject
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
+ (struct CGImage *)newThumbnailForTargetSize:(struct CGSize)arg1 imageSize:(struct CGSize)arg2 imageSource:(struct CGImageSource *)arg3 atIndex:(unsigned long long)arg4 mode:(long long)arg5 scale:(double)arg6;
|
|
||||||
+ (struct CGImage *)newThumbnailForTargetSize:(struct CGSize)arg1 imageSize:(struct CGSize)arg2 imageSource:(struct CGImageSource *)arg3 mode:(long long)arg4 scale:(double)arg5;
|
|
||||||
+ (BOOL)persistCPBitmapWithImage:(struct CGImage *)arg1 url:(id)arg2;
|
|
||||||
+ (void)sampleImageEdges:(char *)arg1 usingRect:(struct CGRect)arg2 forMostlyWhitePixels:(unsigned long long *)arg3 otherPixels:(unsigned long long *)arg4 bytesPerRow:(long long)arg5;
|
|
||||||
+ (struct CGSize)imageRefPxSize:(struct CGImage *)arg1;
|
|
||||||
+ (struct CGSize)imageSourcePxSize:(struct CGImageSource *)arg1;
|
|
||||||
|
|
||||||
@end
|
|
||||||
|
|
||||||
@interface IMOneTimeCodeUtilities : NSObject
|
@interface IMOneTimeCodeUtilities : NSObject
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
@@ -1434,17 +1413,6 @@ extern NSURL* IMAttachmentPreviewFileURL(NSURL *attachmentURL, NSString *extensi
|
|||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@interface IMImagePreviewGenerator : IMPreviewGenerator <IMPreviewGeneratorProtocol, IMUTITypeInformation>
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
+ (struct CGImage *)newThumbnailFillToSize:(struct CGSize)arg1 imagePxSize:(struct CGSize)arg2 imageSource:(struct CGImageSource *)arg3 scale:(double)arg4;
|
|
||||||
+ (struct CGImage *)newPreviewFromSourceURL:(id)arg1 withPreviewConstraints:(struct IMPreviewConstraints)arg2 error:(id *)arg3;
|
|
||||||
+ (id)UTITypes;
|
|
||||||
+ (id)fetchUTITypes;
|
|
||||||
|
|
||||||
@end
|
|
||||||
|
|
||||||
|
|
||||||
@interface IMAKAppleIDAuthenticationController : NSObject
|
@interface IMAKAppleIDAuthenticationController : NSObject
|
||||||
{
|
{
|
||||||
@@ -1704,4 +1672,3 @@ extern NSURL* IMAttachmentPreviewFileURL(NSURL *attachmentURL, NSString *extensi
|
|||||||
- (id)initWithSerializedError_im:(struct NSDictionary *)arg1;
|
- (id)initWithSerializedError_im:(struct NSDictionary *)arg1;
|
||||||
@end
|
@end
|
||||||
|
|
||||||
extern NSString *IMAssociatedMessageDecodeGUID(NSString* guid);
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
INSTALL_PATH := /usr/share/kordophone
|
INSTALL_PATH := /usr/share/kordophone
|
||||||
|
|
||||||
.PHONY: build/Release/kordophoned
|
|
||||||
build/Release/kordophoned:
|
build/Release/kordophoned:
|
||||||
xcodebuild
|
xcodebuild
|
||||||
|
|
||||||
@@ -8,6 +7,7 @@ build/Release/kordophoned:
|
|||||||
install: build/Release/kordophoned
|
install: build/Release/kordophoned
|
||||||
install -d $(INSTALL_PATH)
|
install -d $(INSTALL_PATH)
|
||||||
install build/Release/kordophoned $(INSTALL_PATH)
|
install build/Release/kordophoned $(INSTALL_PATH)
|
||||||
|
cp -rf build/Release/CocoaHTTPServer.framework $(INSTALL_PATH)
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -Rf build
|
rm -Rf build
|
||||||
@@ -14,61 +14,79 @@
|
|||||||
1AA43E95219EC38E00EDF1A7 /* MBIMHTTPUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = 1AA43E94219EC38E00EDF1A7 /* MBIMHTTPUtilities.m */; };
|
1AA43E95219EC38E00EDF1A7 /* MBIMHTTPUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = 1AA43E94219EC38E00EDF1A7 /* MBIMHTTPUtilities.m */; };
|
||||||
1AAB32B121F82EB7004A2A72 /* MBIMLogging.m in Sources */ = {isa = PBXBuildFile; fileRef = 1AAB32B021F82EB7004A2A72 /* MBIMLogging.m */; };
|
1AAB32B121F82EB7004A2A72 /* MBIMLogging.m in Sources */ = {isa = PBXBuildFile; fileRef = 1AAB32B021F82EB7004A2A72 /* MBIMLogging.m */; };
|
||||||
1AAB32B421F837BB004A2A72 /* hookAgent.sh in CopyFiles */ = {isa = PBXBuildFile; fileRef = CD83E1B5219BF78E00F4CCEA /* hookAgent.sh */; };
|
1AAB32B421F837BB004A2A72 /* hookAgent.sh in CopyFiles */ = {isa = PBXBuildFile; fileRef = CD83E1B5219BF78E00F4CCEA /* hookAgent.sh */; };
|
||||||
|
1ACFCF2A219EB2AC00E2C237 /* HTTPConnection.h in Headers */ = {isa = PBXBuildFile; fileRef = 1ACFCE00219EB2AB00E2C237 /* HTTPConnection.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||||
|
1ACFCF2B219EB2AC00E2C237 /* HTTPLogging.h in Headers */ = {isa = PBXBuildFile; fileRef = 1ACFCE01219EB2AB00E2C237 /* HTTPLogging.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||||
|
1ACFCF2C219EB2AC00E2C237 /* HTTPMessage.h in Headers */ = {isa = PBXBuildFile; fileRef = 1ACFCE02219EB2AB00E2C237 /* HTTPMessage.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||||
|
1ACFCF2D219EB2AC00E2C237 /* WebSocket.h in Headers */ = {isa = PBXBuildFile; fileRef = 1ACFCE03219EB2AB00E2C237 /* WebSocket.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||||
|
1ACFCF2E219EB2AC00E2C237 /* HTTPAuthenticationRequest.h in Headers */ = {isa = PBXBuildFile; fileRef = 1ACFCE04219EB2AB00E2C237 /* HTTPAuthenticationRequest.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||||
|
1ACFCF2F219EB2AC00E2C237 /* HTTPAsyncFileResponse.h in Headers */ = {isa = PBXBuildFile; fileRef = 1ACFCE06219EB2AB00E2C237 /* HTTPAsyncFileResponse.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||||
1ACFCF30219EB2AC00E2C237 /* HTTPErrorResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ACFCE07219EB2AB00E2C237 /* HTTPErrorResponse.m */; };
|
1ACFCF30219EB2AC00E2C237 /* HTTPErrorResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ACFCE07219EB2AB00E2C237 /* HTTPErrorResponse.m */; };
|
||||||
1ACFCF31219EB2AC00E2C237 /* HTTPDataResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ACFCE08219EB2AB00E2C237 /* HTTPDataResponse.m */; };
|
1ACFCF31219EB2AC00E2C237 /* HTTPDataResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ACFCE08219EB2AB00E2C237 /* HTTPDataResponse.m */; };
|
||||||
|
1ACFCF32219EB2AC00E2C237 /* HTTPRedirectResponse.h in Headers */ = {isa = PBXBuildFile; fileRef = 1ACFCE09219EB2AB00E2C237 /* HTTPRedirectResponse.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||||
1ACFCF33219EB2AC00E2C237 /* HTTPDynamicFileResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ACFCE0A219EB2AB00E2C237 /* HTTPDynamicFileResponse.m */; };
|
1ACFCF33219EB2AC00E2C237 /* HTTPDynamicFileResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ACFCE0A219EB2AB00E2C237 /* HTTPDynamicFileResponse.m */; };
|
||||||
1ACFCF34219EB2AC00E2C237 /* HTTPFileResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ACFCE0B219EB2AB00E2C237 /* HTTPFileResponse.m */; };
|
1ACFCF34219EB2AC00E2C237 /* HTTPFileResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ACFCE0B219EB2AB00E2C237 /* HTTPFileResponse.m */; };
|
||||||
1ACFCF35219EB2AC00E2C237 /* HTTPAsyncFileResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ACFCE0C219EB2AB00E2C237 /* HTTPAsyncFileResponse.m */; };
|
1ACFCF35219EB2AC00E2C237 /* HTTPAsyncFileResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ACFCE0C219EB2AB00E2C237 /* HTTPAsyncFileResponse.m */; };
|
||||||
1ACFCF36219EB2AC00E2C237 /* HTTPRedirectResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ACFCE0D219EB2AB00E2C237 /* HTTPRedirectResponse.m */; };
|
1ACFCF36219EB2AC00E2C237 /* HTTPRedirectResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ACFCE0D219EB2AB00E2C237 /* HTTPRedirectResponse.m */; };
|
||||||
|
1ACFCF37219EB2AC00E2C237 /* HTTPDataResponse.h in Headers */ = {isa = PBXBuildFile; fileRef = 1ACFCE0E219EB2AB00E2C237 /* HTTPDataResponse.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||||
|
1ACFCF38219EB2AC00E2C237 /* HTTPErrorResponse.h in Headers */ = {isa = PBXBuildFile; fileRef = 1ACFCE0F219EB2AB00E2C237 /* HTTPErrorResponse.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||||
|
1ACFCF39219EB2AC00E2C237 /* HTTPFileResponse.h in Headers */ = {isa = PBXBuildFile; fileRef = 1ACFCE10219EB2AB00E2C237 /* HTTPFileResponse.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||||
|
1ACFCF3A219EB2AC00E2C237 /* HTTPDynamicFileResponse.h in Headers */ = {isa = PBXBuildFile; fileRef = 1ACFCE11219EB2AB00E2C237 /* HTTPDynamicFileResponse.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||||
|
1ACFCF3B219EB2AC00E2C237 /* HTTPServer.h in Headers */ = {isa = PBXBuildFile; fileRef = 1ACFCE12219EB2AB00E2C237 /* HTTPServer.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||||
1ACFCF3C219EB2AC00E2C237 /* HTTPMessage.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ACFCE13219EB2AB00E2C237 /* HTTPMessage.m */; };
|
1ACFCF3C219EB2AC00E2C237 /* HTTPMessage.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ACFCE13219EB2AB00E2C237 /* HTTPMessage.m */; };
|
||||||
1ACFCF3D219EB2AC00E2C237 /* HTTPConnection.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ACFCE14219EB2AB00E2C237 /* HTTPConnection.m */; };
|
1ACFCF3D219EB2AC00E2C237 /* HTTPConnection.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ACFCE14219EB2AB00E2C237 /* HTTPConnection.m */; };
|
||||||
1ACFCF3E219EB2AC00E2C237 /* WebSocket.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ACFCE15219EB2AB00E2C237 /* WebSocket.m */; };
|
1ACFCF3E219EB2AC00E2C237 /* WebSocket.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ACFCE15219EB2AB00E2C237 /* WebSocket.m */; };
|
||||||
|
1ACFCF3F219EB2AC00E2C237 /* HTTPResponse.h in Headers */ = {isa = PBXBuildFile; fileRef = 1ACFCE16219EB2AB00E2C237 /* HTTPResponse.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||||
|
1ACFCF40219EB2AC00E2C237 /* MultipartFormDataParser.h in Headers */ = {isa = PBXBuildFile; fileRef = 1ACFCE18219EB2AB00E2C237 /* MultipartFormDataParser.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||||
|
1ACFCF41219EB2AC00E2C237 /* MultipartMessageHeader.h in Headers */ = {isa = PBXBuildFile; fileRef = 1ACFCE19219EB2AB00E2C237 /* MultipartMessageHeader.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||||
1ACFCF42219EB2AC00E2C237 /* MultipartMessageHeaderField.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ACFCE1A219EB2AB00E2C237 /* MultipartMessageHeaderField.m */; };
|
1ACFCF42219EB2AC00E2C237 /* MultipartMessageHeaderField.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ACFCE1A219EB2AB00E2C237 /* MultipartMessageHeaderField.m */; };
|
||||||
1ACFCF43219EB2AC00E2C237 /* MultipartFormDataParser.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ACFCE1B219EB2AB00E2C237 /* MultipartFormDataParser.m */; };
|
1ACFCF43219EB2AC00E2C237 /* MultipartFormDataParser.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ACFCE1B219EB2AB00E2C237 /* MultipartFormDataParser.m */; };
|
||||||
1ACFCF44219EB2AC00E2C237 /* MultipartMessageHeader.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ACFCE1C219EB2AB00E2C237 /* MultipartMessageHeader.m */; };
|
1ACFCF44219EB2AC00E2C237 /* MultipartMessageHeader.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ACFCE1C219EB2AB00E2C237 /* MultipartMessageHeader.m */; };
|
||||||
|
1ACFCF45219EB2AC00E2C237 /* MultipartMessageHeaderField.h in Headers */ = {isa = PBXBuildFile; fileRef = 1ACFCE1D219EB2AB00E2C237 /* MultipartMessageHeaderField.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||||
1ACFCF46219EB2AC00E2C237 /* HTTPAuthenticationRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ACFCE1E219EB2AB00E2C237 /* HTTPAuthenticationRequest.m */; };
|
1ACFCF46219EB2AC00E2C237 /* HTTPAuthenticationRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ACFCE1E219EB2AB00E2C237 /* HTTPAuthenticationRequest.m */; };
|
||||||
1ACFCF47219EB2AC00E2C237 /* DDNumber.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ACFCE20219EB2AB00E2C237 /* DDNumber.m */; };
|
1ACFCF47219EB2AC00E2C237 /* DDNumber.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ACFCE20219EB2AB00E2C237 /* DDNumber.m */; };
|
||||||
1ACFCF48219EB2AC00E2C237 /* DDData.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ACFCE21219EB2AB00E2C237 /* DDData.m */; };
|
1ACFCF48219EB2AC00E2C237 /* DDData.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ACFCE21219EB2AB00E2C237 /* DDData.m */; };
|
||||||
|
1ACFCF49219EB2AC00E2C237 /* DDRange.h in Headers */ = {isa = PBXBuildFile; fileRef = 1ACFCE22219EB2AB00E2C237 /* DDRange.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||||
|
1ACFCF4A219EB2AC00E2C237 /* DDNumber.h in Headers */ = {isa = PBXBuildFile; fileRef = 1ACFCE23219EB2AB00E2C237 /* DDNumber.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||||
1ACFCF4B219EB2AC00E2C237 /* DDRange.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ACFCE24219EB2AB00E2C237 /* DDRange.m */; };
|
1ACFCF4B219EB2AC00E2C237 /* DDRange.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ACFCE24219EB2AB00E2C237 /* DDRange.m */; };
|
||||||
|
1ACFCF4C219EB2AC00E2C237 /* DDData.h in Headers */ = {isa = PBXBuildFile; fileRef = 1ACFCE25219EB2AB00E2C237 /* DDData.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||||
1ACFCF4D219EB2AC00E2C237 /* HTTPServer.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ACFCE26219EB2AB00E2C237 /* HTTPServer.m */; };
|
1ACFCF4D219EB2AC00E2C237 /* HTTPServer.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ACFCE26219EB2AB00E2C237 /* HTTPServer.m */; };
|
||||||
1ACFCFCA219EB2AC00E2C237 /* DDTTYLogger.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ACFCF09219EB2AC00E2C237 /* DDTTYLogger.m */; };
|
1ACFCFCA219EB2AC00E2C237 /* DDTTYLogger.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ACFCF09219EB2AC00E2C237 /* DDTTYLogger.m */; };
|
||||||
|
1ACFCFCB219EB2AC00E2C237 /* DDLog.h in Headers */ = {isa = PBXBuildFile; fileRef = 1ACFCF0A219EB2AC00E2C237 /* DDLog.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||||
|
1ACFCFCC219EB2AC00E2C237 /* DDAbstractDatabaseLogger.h in Headers */ = {isa = PBXBuildFile; fileRef = 1ACFCF0B219EB2AC00E2C237 /* DDAbstractDatabaseLogger.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||||
1ACFCFCD219EB2AC00E2C237 /* DDASLLogger.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ACFCF0C219EB2AC00E2C237 /* DDASLLogger.m */; };
|
1ACFCFCD219EB2AC00E2C237 /* DDASLLogger.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ACFCF0C219EB2AC00E2C237 /* DDASLLogger.m */; };
|
||||||
|
1ACFCFCE219EB2AC00E2C237 /* DDFileLogger.h in Headers */ = {isa = PBXBuildFile; fileRef = 1ACFCF0D219EB2AC00E2C237 /* DDFileLogger.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||||
|
1ACFCFCF219EB2AC00E2C237 /* ContextFilterLogFormatter.h in Headers */ = {isa = PBXBuildFile; fileRef = 1ACFCF0F219EB2AC00E2C237 /* ContextFilterLogFormatter.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||||
|
1ACFCFD0219EB2AC00E2C237 /* DispatchQueueLogFormatter.h in Headers */ = {isa = PBXBuildFile; fileRef = 1ACFCF10219EB2AC00E2C237 /* DispatchQueueLogFormatter.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||||
1ACFCFD1219EB2AC00E2C237 /* ContextFilterLogFormatter.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ACFCF11219EB2AC00E2C237 /* ContextFilterLogFormatter.m */; };
|
1ACFCFD1219EB2AC00E2C237 /* ContextFilterLogFormatter.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ACFCF11219EB2AC00E2C237 /* ContextFilterLogFormatter.m */; };
|
||||||
1ACFCFD2219EB2AC00E2C237 /* DispatchQueueLogFormatter.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ACFCF12219EB2AC00E2C237 /* DispatchQueueLogFormatter.m */; };
|
1ACFCFD2219EB2AC00E2C237 /* DispatchQueueLogFormatter.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ACFCF12219EB2AC00E2C237 /* DispatchQueueLogFormatter.m */; };
|
||||||
1ACFCFD4219EB2AC00E2C237 /* DDLog.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ACFCF14219EB2AC00E2C237 /* DDLog.m */; };
|
1ACFCFD4219EB2AC00E2C237 /* DDLog.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ACFCF14219EB2AC00E2C237 /* DDLog.m */; };
|
||||||
|
1ACFCFD6219EB2AC00E2C237 /* DDTTYLogger.h in Headers */ = {isa = PBXBuildFile; fileRef = 1ACFCF16219EB2AC00E2C237 /* DDTTYLogger.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||||
1ACFCFD7219EB2AC00E2C237 /* DDAbstractDatabaseLogger.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ACFCF17219EB2AC00E2C237 /* DDAbstractDatabaseLogger.m */; };
|
1ACFCFD7219EB2AC00E2C237 /* DDAbstractDatabaseLogger.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ACFCF17219EB2AC00E2C237 /* DDAbstractDatabaseLogger.m */; };
|
||||||
1ACFCFD8219EB2AC00E2C237 /* DDFileLogger.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ACFCF18219EB2AC00E2C237 /* DDFileLogger.m */; };
|
1ACFCFD8219EB2AC00E2C237 /* DDFileLogger.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ACFCF18219EB2AC00E2C237 /* DDFileLogger.m */; };
|
||||||
|
1ACFCFD9219EB2AC00E2C237 /* DDASLLogger.h in Headers */ = {isa = PBXBuildFile; fileRef = 1ACFCF19219EB2AC00E2C237 /* DDASLLogger.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||||
1ACFCFDA219EB2AC00E2C237 /* GCDAsyncSocket.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ACFCF1B219EB2AC00E2C237 /* GCDAsyncSocket.m */; };
|
1ACFCFDA219EB2AC00E2C237 /* GCDAsyncSocket.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ACFCF1B219EB2AC00E2C237 /* GCDAsyncSocket.m */; };
|
||||||
1ACFCFDF219EB31400E2C237 /* libCocoaHTTPServer.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 1ACFCDE2219EB28A00E2C237 /* libCocoaHTTPServer.a */; };
|
1ACFCFDC219EB2AC00E2C237 /* GCDAsyncSocket.h in Headers */ = {isa = PBXBuildFile; fileRef = 1ACFCF1D219EB2AC00E2C237 /* GCDAsyncSocket.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||||
|
1ACFCFDF219EB31400E2C237 /* CocoaHTTPServer.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1ACFCDE2219EB28A00E2C237 /* CocoaHTTPServer.framework */; };
|
||||||
1AD8936E21EFD986009B599A /* MBIMUploadAttachmentOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = 1AD8936D21EFD986009B599A /* MBIMUploadAttachmentOperation.m */; };
|
1AD8936E21EFD986009B599A /* MBIMUploadAttachmentOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = 1AD8936D21EFD986009B599A /* MBIMUploadAttachmentOperation.m */; };
|
||||||
CD14F18E219E2DB400E7DD22 /* CryptoTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CD14F18D219E2DB400E7DD22 /* CryptoTests.m */; };
|
CD14F18E219E2DB400E7DD22 /* CryptoTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CD14F18D219E2DB400E7DD22 /* CryptoTests.m */; };
|
||||||
CD14F1A1219FE7D600E7DD22 /* MBIMUpdatePollOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = CD14F1A0219FE7D600E7DD22 /* MBIMUpdatePollOperation.m */; };
|
CD14F1A1219FE7D600E7DD22 /* MBIMUpdatePollOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = CD14F1A0219FE7D600E7DD22 /* MBIMUpdatePollOperation.m */; };
|
||||||
CD14F1A4219FF22700E7DD22 /* IMMessageItem+Encoded.m in Sources */ = {isa = PBXBuildFile; fileRef = CD14F1A3219FF22700E7DD22 /* IMMessageItem+Encoded.m */; };
|
CD14F1A4219FF22700E7DD22 /* IMMessageItem+Encoded.m in Sources */ = {isa = PBXBuildFile; fileRef = CD14F1A3219FF22700E7DD22 /* IMMessageItem+Encoded.m */; };
|
||||||
CD14F1AA219FF3B800E7DD22 /* MBIMUpdateQueue.m in Sources */ = {isa = PBXBuildFile; fileRef = CD14F1A9219FF3B800E7DD22 /* MBIMUpdateQueue.m */; };
|
CD14F1AA219FF3B800E7DD22 /* MBIMUpdateQueue.m in Sources */ = {isa = PBXBuildFile; fileRef = CD14F1A9219FF3B800E7DD22 /* MBIMUpdateQueue.m */; };
|
||||||
CD14F1AD219FFAE100E7DD22 /* MBIMConcurrentHTTPServer.m in Sources */ = {isa = PBXBuildFile; fileRef = CD14F1AC219FFAE100E7DD22 /* MBIMConcurrentHTTPServer.m */; };
|
CD14F1AD219FFAE100E7DD22 /* MBIMConcurrentHTTPServer.m in Sources */ = {isa = PBXBuildFile; fileRef = CD14F1AC219FFAE100E7DD22 /* MBIMConcurrentHTTPServer.m */; };
|
||||||
CD2782BC29527FE500C0C030 /* IMSharedUtilities.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CD2782BB29527FE500C0C030 /* IMSharedUtilities.framework */; };
|
|
||||||
CD2782BF2952832B00C0C030 /* MBIMImageUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = CD2782BE2952832B00C0C030 /* MBIMImageUtils.m */; };
|
|
||||||
CD2782FE2952875F00C0C030 /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CD2782FD2952875F00C0C030 /* CoreGraphics.framework */; };
|
|
||||||
CD2783002952876700C0C030 /* ImageIO.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CD2782FF2952876700C0C030 /* ImageIO.framework */; };
|
|
||||||
CD2ECEC2269539100055E302 /* MBIMAuthenticateOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = CD2ECEC1269539100055E302 /* MBIMAuthenticateOperation.m */; };
|
CD2ECEC2269539100055E302 /* MBIMAuthenticateOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = CD2ECEC1269539100055E302 /* MBIMAuthenticateOperation.m */; };
|
||||||
CD2ECEC526953F2A0055E302 /* MBIMAuthToken.m in Sources */ = {isa = PBXBuildFile; fileRef = CD2ECEC426953F2A0055E302 /* MBIMAuthToken.m */; };
|
CD2ECEC526953F2A0055E302 /* MBIMAuthToken.m in Sources */ = {isa = PBXBuildFile; fileRef = CD2ECEC426953F2A0055E302 /* MBIMAuthToken.m */; };
|
||||||
CD3F62B1297769F2004305D9 /* MBIMURLUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = CD3F62B0297769F2004305D9 /* MBIMURLUtilities.m */; };
|
|
||||||
CD602056219B5DFD0024D9C5 /* MBIMBridgeOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = CD602055219B5DFD0024D9C5 /* MBIMBridgeOperation.m */; };
|
CD602056219B5DFD0024D9C5 /* MBIMBridgeOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = CD602055219B5DFD0024D9C5 /* MBIMBridgeOperation.m */; };
|
||||||
CD60205C219B623F0024D9C5 /* MBIMMessagesListOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = CD60205B219B623F0024D9C5 /* MBIMMessagesListOperation.m */; };
|
CD60205C219B623F0024D9C5 /* MBIMMessagesListOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = CD60205B219B623F0024D9C5 /* MBIMMessagesListOperation.m */; };
|
||||||
CD60205F219B674B0024D9C5 /* MBIMConversationListOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = CD60205E219B674B0024D9C5 /* MBIMConversationListOperation.m */; };
|
CD60205F219B674B0024D9C5 /* MBIMConversationListOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = CD60205E219B674B0024D9C5 /* MBIMConversationListOperation.m */; };
|
||||||
CD602062219B68950024D9C5 /* MBIMSendMessageOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = CD602061219B68950024D9C5 /* MBIMSendMessageOperation.m */; };
|
CD602062219B68950024D9C5 /* MBIMSendMessageOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = CD602061219B68950024D9C5 /* MBIMSendMessageOperation.m */; };
|
||||||
CD83E156219BE10A00F4CCEA /* hooking.m in Sources */ = {isa = PBXBuildFile; fileRef = CD83E155219BE10A00F4CCEA /* hooking.m */; };
|
CD83E156219BE10A00F4CCEA /* hooking.m in Sources */ = {isa = PBXBuildFile; fileRef = CD83E155219BE10A00F4CCEA /* hooking.m */; };
|
||||||
CD83E166219BE91600F4CCEA /* agentHook.m in Sources */ = {isa = PBXBuildFile; fileRef = CD83E165219BE91600F4CCEA /* agentHook.m */; };
|
CD83E166219BE91600F4CCEA /* agentHook.m in Sources */ = {isa = PBXBuildFile; fileRef = CD83E165219BE91600F4CCEA /* agentHook.m */; };
|
||||||
CD936A32289B353F0093A1AC /* MBIMErrorResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = CD936A31289B353F0093A1AC /* MBIMErrorResponse.m */; };
|
CDD8C98426977D2A00551AE5 /* MBIMAliasValidationOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = CDD8C98326977D2A00551AE5 /* MBIMAliasValidationOperation.m */; };
|
||||||
CD936A35289B47D60093A1AC /* MBIMVersionOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = CD936A34289B47D50093A1AC /* MBIMVersionOperation.m */; };
|
CDD8C9862697996800551AE5 /* IDS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CDD8C9852697996700551AE5 /* IDS.framework */; };
|
||||||
CD936A39289B49FC0093A1AC /* MBIMStatusOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = CD936A38289B49FC0093A1AC /* MBIMStatusOperation.m */; };
|
|
||||||
CDA64B472DFCBF3000E9B07E /* MBIMPingPongWebSocket.m in Sources */ = {isa = PBXBuildFile; fileRef = CDA64B462DFCBF3000E9B07E /* MBIMPingPongWebSocket.m */; };
|
|
||||||
CDDCF78D283F398C0087ABDF /* MBIMDeleteConversationOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = CDDCF78C283F398C0087ABDF /* MBIMDeleteConversationOperation.m */; };
|
|
||||||
CDE4556421A3578A0041F5DD /* IMChat+Encoded.m in Sources */ = {isa = PBXBuildFile; fileRef = CDE4556321A3578A0041F5DD /* IMChat+Encoded.m */; };
|
CDE4556421A3578A0041F5DD /* IMChat+Encoded.m in Sources */ = {isa = PBXBuildFile; fileRef = CDE4556321A3578A0041F5DD /* IMChat+Encoded.m */; };
|
||||||
CDE455A121A365AD0041F5DD /* MBIMMarkOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = CDE455A021A365AD0041F5DD /* MBIMMarkOperation.m */; };
|
CDE455A121A365AD0041F5DD /* MBIMMarkOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = CDE455A021A365AD0041F5DD /* MBIMMarkOperation.m */; };
|
||||||
CDE455A421A5308D0041F5DD /* MBIMFetchAttachmentOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = CDE455A321A5308D0041F5DD /* MBIMFetchAttachmentOperation.m */; };
|
CDE455A421A5308D0041F5DD /* MBIMFetchAttachmentOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = CDE455A321A5308D0041F5DD /* MBIMFetchAttachmentOperation.m */; };
|
||||||
CDE455A721A531ED0041F5DD /* MBIMDataResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = CDE455A621A531ED0041F5DD /* MBIMDataResponse.m */; };
|
CDE455A721A531ED0041F5DD /* MBIMDataResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = CDE455A621A531ED0041F5DD /* MBIMDataResponse.m */; };
|
||||||
CDEFF9FE2CACC7A700063C52 /* MBIMResolveHandleOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = CDEFF9FD2CACC7A700063C52 /* MBIMResolveHandleOperation.m */; };
|
|
||||||
CDF62335219A895D00690038 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = CDF62334219A895D00690038 /* main.m */; };
|
CDF62335219A895D00690038 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = CDF62334219A895D00690038 /* main.m */; };
|
||||||
CDF62339219A8A5600690038 /* MBIMBridge.h in Sources */ = {isa = PBXBuildFile; fileRef = 1A0C4469219A4BC300F2AC00 /* MBIMBridge.h */; };
|
CDF62339219A8A5600690038 /* MBIMBridge.h in Sources */ = {isa = PBXBuildFile; fileRef = 1A0C4469219A4BC300F2AC00 /* MBIMBridge.h */; };
|
||||||
CDF6233A219A8A5600690038 /* MBIMBridge.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A0C446A219A4BC300F2AC00 /* MBIMBridge.m */; };
|
CDF6233A219A8A5600690038 /* MBIMBridge.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A0C446A219A4BC300F2AC00 /* MBIMBridge.m */; };
|
||||||
@@ -130,7 +148,7 @@
|
|||||||
1AAB32AF21F82EB7004A2A72 /* MBIMLogging.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MBIMLogging.h; sourceTree = "<group>"; };
|
1AAB32AF21F82EB7004A2A72 /* MBIMLogging.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MBIMLogging.h; sourceTree = "<group>"; };
|
||||||
1AAB32B021F82EB7004A2A72 /* MBIMLogging.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MBIMLogging.m; sourceTree = "<group>"; };
|
1AAB32B021F82EB7004A2A72 /* MBIMLogging.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MBIMLogging.m; sourceTree = "<group>"; };
|
||||||
1AAB32B221F835BD004A2A72 /* KPServer.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KPServer.pch; sourceTree = "<group>"; };
|
1AAB32B221F835BD004A2A72 /* KPServer.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KPServer.pch; sourceTree = "<group>"; };
|
||||||
1ACFCDE2219EB28A00E2C237 /* libCocoaHTTPServer.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libCocoaHTTPServer.a; sourceTree = BUILT_PRODUCTS_DIR; };
|
1ACFCDE2219EB28A00E2C237 /* CocoaHTTPServer.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = CocoaHTTPServer.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
1ACFCDFC219EB2AB00E2C237 /* LICENSE.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = LICENSE.txt; sourceTree = "<group>"; };
|
1ACFCDFC219EB2AB00E2C237 /* LICENSE.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = LICENSE.txt; sourceTree = "<group>"; };
|
||||||
1ACFCDFE219EB2AB00E2C237 /* README.markdown */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.markdown; sourceTree = "<group>"; };
|
1ACFCDFE219EB2AB00E2C237 /* README.markdown */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.markdown; sourceTree = "<group>"; };
|
||||||
1ACFCE00219EB2AB00E2C237 /* HTTPConnection.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HTTPConnection.h; sourceTree = "<group>"; };
|
1ACFCE00219EB2AB00E2C237 /* HTTPConnection.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HTTPConnection.h; sourceTree = "<group>"; };
|
||||||
@@ -203,17 +221,10 @@
|
|||||||
CD14F1A9219FF3B800E7DD22 /* MBIMUpdateQueue.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MBIMUpdateQueue.m; sourceTree = "<group>"; };
|
CD14F1A9219FF3B800E7DD22 /* MBIMUpdateQueue.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MBIMUpdateQueue.m; sourceTree = "<group>"; };
|
||||||
CD14F1AB219FFAE100E7DD22 /* MBIMConcurrentHTTPServer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MBIMConcurrentHTTPServer.h; sourceTree = "<group>"; };
|
CD14F1AB219FFAE100E7DD22 /* MBIMConcurrentHTTPServer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MBIMConcurrentHTTPServer.h; sourceTree = "<group>"; };
|
||||||
CD14F1AC219FFAE100E7DD22 /* MBIMConcurrentHTTPServer.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MBIMConcurrentHTTPServer.m; sourceTree = "<group>"; };
|
CD14F1AC219FFAE100E7DD22 /* MBIMConcurrentHTTPServer.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MBIMConcurrentHTTPServer.m; sourceTree = "<group>"; };
|
||||||
CD2782BB29527FE500C0C030 /* IMSharedUtilities.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = IMSharedUtilities.framework; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX14.0.Internal.sdk/System/Library/PrivateFrameworks/IMSharedUtilities.framework; sourceTree = DEVELOPER_DIR; };
|
|
||||||
CD2782BD2952832B00C0C030 /* MBIMImageUtils.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MBIMImageUtils.h; sourceTree = "<group>"; };
|
|
||||||
CD2782BE2952832B00C0C030 /* MBIMImageUtils.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MBIMImageUtils.m; sourceTree = "<group>"; };
|
|
||||||
CD2782FD2952875F00C0C030 /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; };
|
|
||||||
CD2782FF2952876700C0C030 /* ImageIO.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ImageIO.framework; path = System/Library/Frameworks/ImageIO.framework; sourceTree = SDKROOT; };
|
|
||||||
CD2ECEC0269539100055E302 /* MBIMAuthenticateOperation.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MBIMAuthenticateOperation.h; sourceTree = "<group>"; };
|
CD2ECEC0269539100055E302 /* MBIMAuthenticateOperation.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MBIMAuthenticateOperation.h; sourceTree = "<group>"; };
|
||||||
CD2ECEC1269539100055E302 /* MBIMAuthenticateOperation.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MBIMAuthenticateOperation.m; sourceTree = "<group>"; };
|
CD2ECEC1269539100055E302 /* MBIMAuthenticateOperation.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MBIMAuthenticateOperation.m; sourceTree = "<group>"; };
|
||||||
CD2ECEC326953F2A0055E302 /* MBIMAuthToken.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MBIMAuthToken.h; sourceTree = "<group>"; };
|
CD2ECEC326953F2A0055E302 /* MBIMAuthToken.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MBIMAuthToken.h; sourceTree = "<group>"; };
|
||||||
CD2ECEC426953F2A0055E302 /* MBIMAuthToken.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MBIMAuthToken.m; sourceTree = "<group>"; };
|
CD2ECEC426953F2A0055E302 /* MBIMAuthToken.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MBIMAuthToken.m; sourceTree = "<group>"; };
|
||||||
CD3F62AF297769F2004305D9 /* MBIMURLUtilities.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MBIMURLUtilities.h; sourceTree = "<group>"; };
|
|
||||||
CD3F62B0297769F2004305D9 /* MBIMURLUtilities.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MBIMURLUtilities.m; sourceTree = "<group>"; };
|
|
||||||
CD602054219B5DFD0024D9C5 /* MBIMBridgeOperation.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MBIMBridgeOperation.h; sourceTree = "<group>"; };
|
CD602054219B5DFD0024D9C5 /* MBIMBridgeOperation.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MBIMBridgeOperation.h; sourceTree = "<group>"; };
|
||||||
CD602055219B5DFD0024D9C5 /* MBIMBridgeOperation.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MBIMBridgeOperation.m; sourceTree = "<group>"; };
|
CD602055219B5DFD0024D9C5 /* MBIMBridgeOperation.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MBIMBridgeOperation.m; sourceTree = "<group>"; };
|
||||||
CD60205A219B623F0024D9C5 /* MBIMMessagesListOperation.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MBIMMessagesListOperation.h; sourceTree = "<group>"; };
|
CD60205A219B623F0024D9C5 /* MBIMMessagesListOperation.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MBIMMessagesListOperation.h; sourceTree = "<group>"; };
|
||||||
@@ -227,17 +238,9 @@
|
|||||||
CD83E161219BE91500F4CCEA /* libagentHook.dylib */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.dylib"; includeInIndex = 0; path = libagentHook.dylib; sourceTree = BUILT_PRODUCTS_DIR; };
|
CD83E161219BE91500F4CCEA /* libagentHook.dylib */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.dylib"; includeInIndex = 0; path = libagentHook.dylib; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
CD83E165219BE91600F4CCEA /* agentHook.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = agentHook.m; sourceTree = "<group>"; };
|
CD83E165219BE91600F4CCEA /* agentHook.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = agentHook.m; sourceTree = "<group>"; };
|
||||||
CD83E1B5219BF78E00F4CCEA /* hookAgent.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = hookAgent.sh; sourceTree = "<group>"; };
|
CD83E1B5219BF78E00F4CCEA /* hookAgent.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = hookAgent.sh; sourceTree = "<group>"; };
|
||||||
CD936A2F289B31740093A1AC /* kordophoned-RestrictedEntitlements.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "kordophoned-RestrictedEntitlements.plist"; sourceTree = "<group>"; };
|
CDD8C98226977D2A00551AE5 /* MBIMAliasValidationOperation.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MBIMAliasValidationOperation.h; sourceTree = "<group>"; };
|
||||||
CD936A30289B353F0093A1AC /* MBIMErrorResponse.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MBIMErrorResponse.h; sourceTree = "<group>"; };
|
CDD8C98326977D2A00551AE5 /* MBIMAliasValidationOperation.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MBIMAliasValidationOperation.m; sourceTree = "<group>"; };
|
||||||
CD936A31289B353F0093A1AC /* MBIMErrorResponse.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MBIMErrorResponse.m; sourceTree = "<group>"; };
|
CDD8C9852697996700551AE5 /* IDS.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = IDS.framework; path = System/Library/PrivateFrameworks/IDS.framework; sourceTree = SDKROOT; };
|
||||||
CD936A33289B47D50093A1AC /* MBIMVersionOperation.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MBIMVersionOperation.h; sourceTree = "<group>"; };
|
|
||||||
CD936A34289B47D50093A1AC /* MBIMVersionOperation.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MBIMVersionOperation.m; sourceTree = "<group>"; };
|
|
||||||
CD936A37289B49FC0093A1AC /* MBIMStatusOperation.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MBIMStatusOperation.h; sourceTree = "<group>"; };
|
|
||||||
CD936A38289B49FC0093A1AC /* MBIMStatusOperation.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MBIMStatusOperation.m; sourceTree = "<group>"; };
|
|
||||||
CDA64B452DFCBF3000E9B07E /* MBIMPingPongWebSocket.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MBIMPingPongWebSocket.h; sourceTree = "<group>"; };
|
|
||||||
CDA64B462DFCBF3000E9B07E /* MBIMPingPongWebSocket.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MBIMPingPongWebSocket.m; sourceTree = "<group>"; };
|
|
||||||
CDDCF78B283F398C0087ABDF /* MBIMDeleteConversationOperation.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MBIMDeleteConversationOperation.h; sourceTree = "<group>"; };
|
|
||||||
CDDCF78C283F398C0087ABDF /* MBIMDeleteConversationOperation.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MBIMDeleteConversationOperation.m; sourceTree = "<group>"; };
|
|
||||||
CDE4556221A3578A0041F5DD /* IMChat+Encoded.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "IMChat+Encoded.h"; sourceTree = "<group>"; };
|
CDE4556221A3578A0041F5DD /* IMChat+Encoded.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "IMChat+Encoded.h"; sourceTree = "<group>"; };
|
||||||
CDE4556321A3578A0041F5DD /* IMChat+Encoded.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "IMChat+Encoded.m"; sourceTree = "<group>"; };
|
CDE4556321A3578A0041F5DD /* IMChat+Encoded.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "IMChat+Encoded.m"; sourceTree = "<group>"; };
|
||||||
CDE4559F21A365AD0041F5DD /* MBIMMarkOperation.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MBIMMarkOperation.h; sourceTree = "<group>"; };
|
CDE4559F21A365AD0041F5DD /* MBIMMarkOperation.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MBIMMarkOperation.h; sourceTree = "<group>"; };
|
||||||
@@ -246,8 +249,6 @@
|
|||||||
CDE455A321A5308D0041F5DD /* MBIMFetchAttachmentOperation.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MBIMFetchAttachmentOperation.m; sourceTree = "<group>"; };
|
CDE455A321A5308D0041F5DD /* MBIMFetchAttachmentOperation.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MBIMFetchAttachmentOperation.m; sourceTree = "<group>"; };
|
||||||
CDE455A521A531ED0041F5DD /* MBIMDataResponse.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MBIMDataResponse.h; sourceTree = "<group>"; };
|
CDE455A521A531ED0041F5DD /* MBIMDataResponse.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MBIMDataResponse.h; sourceTree = "<group>"; };
|
||||||
CDE455A621A531ED0041F5DD /* MBIMDataResponse.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MBIMDataResponse.m; sourceTree = "<group>"; };
|
CDE455A621A531ED0041F5DD /* MBIMDataResponse.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MBIMDataResponse.m; sourceTree = "<group>"; };
|
||||||
CDEFF9FC2CACC7A700063C52 /* MBIMResolveHandleOperation.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MBIMResolveHandleOperation.h; sourceTree = "<group>"; };
|
|
||||||
CDEFF9FD2CACC7A700063C52 /* MBIMResolveHandleOperation.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MBIMResolveHandleOperation.m; sourceTree = "<group>"; };
|
|
||||||
CDF62332219A895D00690038 /* kordophoned */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = kordophoned; sourceTree = BUILT_PRODUCTS_DIR; };
|
CDF62332219A895D00690038 /* kordophoned */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = kordophoned; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
CDF62334219A895D00690038 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = "<group>"; };
|
CDF62334219A895D00690038 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = "<group>"; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
@@ -278,11 +279,9 @@
|
|||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
CD2782BC29527FE500C0C030 /* IMSharedUtilities.framework in Frameworks */,
|
CDD8C9862697996800551AE5 /* IDS.framework in Frameworks */,
|
||||||
1A257CCB23A8681200A4A2C8 /* Security.framework in Frameworks */,
|
1A257CCB23A8681200A4A2C8 /* Security.framework in Frameworks */,
|
||||||
CD2783002952876700C0C030 /* ImageIO.framework in Frameworks */,
|
1ACFCFDF219EB31400E2C237 /* CocoaHTTPServer.framework in Frameworks */,
|
||||||
1ACFCFDF219EB31400E2C237 /* libCocoaHTTPServer.a in Frameworks */,
|
|
||||||
CD2782FE2952875F00C0C030 /* CoreGraphics.framework in Frameworks */,
|
|
||||||
1A257CC923A867EF00A4A2C8 /* IMCore.framework in Frameworks */,
|
1A257CC923A867EF00A4A2C8 /* IMCore.framework in Frameworks */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
@@ -310,7 +309,7 @@
|
|||||||
CDF62332219A895D00690038 /* kordophoned */,
|
CDF62332219A895D00690038 /* kordophoned */,
|
||||||
CD83E161219BE91500F4CCEA /* libagentHook.dylib */,
|
CD83E161219BE91500F4CCEA /* libagentHook.dylib */,
|
||||||
CD14F18B219E2DB400E7DD22 /* Tests.xctest */,
|
CD14F18B219E2DB400E7DD22 /* Tests.xctest */,
|
||||||
1ACFCDE2219EB28A00E2C237 /* libCocoaHTTPServer.a */,
|
1ACFCDE2219EB28A00E2C237 /* CocoaHTTPServer.framework */,
|
||||||
);
|
);
|
||||||
name = Products;
|
name = Products;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -328,9 +327,7 @@
|
|||||||
1A0C445E219A45B400F2AC00 /* Frameworks */ = {
|
1A0C445E219A45B400F2AC00 /* Frameworks */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
CD2782FF2952876700C0C030 /* ImageIO.framework */,
|
CDD8C9852697996700551AE5 /* IDS.framework */,
|
||||||
CD2782FD2952875F00C0C030 /* CoreGraphics.framework */,
|
|
||||||
CD2782BB29527FE500C0C030 /* IMSharedUtilities.framework */,
|
|
||||||
1A257CCA23A8681200A4A2C8 /* Security.framework */,
|
1A257CCA23A8681200A4A2C8 /* Security.framework */,
|
||||||
1A257CC823A867EF00A4A2C8 /* IMCore.framework */,
|
1A257CC823A867EF00A4A2C8 /* IMCore.framework */,
|
||||||
);
|
);
|
||||||
@@ -352,8 +349,6 @@
|
|||||||
CD14F1AC219FFAE100E7DD22 /* MBIMConcurrentHTTPServer.m */,
|
CD14F1AC219FFAE100E7DD22 /* MBIMConcurrentHTTPServer.m */,
|
||||||
1ACFCFE2219EB45300E2C237 /* MBIMHTTPConnection.h */,
|
1ACFCFE2219EB45300E2C237 /* MBIMHTTPConnection.h */,
|
||||||
1ACFCFE3219EB45300E2C237 /* MBIMHTTPConnection.m */,
|
1ACFCFE3219EB45300E2C237 /* MBIMHTTPConnection.m */,
|
||||||
CDA64B452DFCBF3000E9B07E /* MBIMPingPongWebSocket.h */,
|
|
||||||
CDA64B462DFCBF3000E9B07E /* MBIMPingPongWebSocket.m */,
|
|
||||||
);
|
);
|
||||||
path = Bridge;
|
path = Bridge;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -365,14 +360,8 @@
|
|||||||
1AA43E8E219EBB2D00EDF1A7 /* MBIMJSONDataResponse.m */,
|
1AA43E8E219EBB2D00EDF1A7 /* MBIMJSONDataResponse.m */,
|
||||||
CDE455A521A531ED0041F5DD /* MBIMDataResponse.h */,
|
CDE455A521A531ED0041F5DD /* MBIMDataResponse.h */,
|
||||||
CDE455A621A531ED0041F5DD /* MBIMDataResponse.m */,
|
CDE455A621A531ED0041F5DD /* MBIMDataResponse.m */,
|
||||||
CD936A30289B353F0093A1AC /* MBIMErrorResponse.h */,
|
|
||||||
CD936A31289B353F0093A1AC /* MBIMErrorResponse.m */,
|
|
||||||
1AA43E93219EC38E00EDF1A7 /* MBIMHTTPUtilities.h */,
|
1AA43E93219EC38E00EDF1A7 /* MBIMHTTPUtilities.h */,
|
||||||
1AA43E94219EC38E00EDF1A7 /* MBIMHTTPUtilities.m */,
|
1AA43E94219EC38E00EDF1A7 /* MBIMHTTPUtilities.m */,
|
||||||
CD2782BD2952832B00C0C030 /* MBIMImageUtils.h */,
|
|
||||||
CD2782BE2952832B00C0C030 /* MBIMImageUtils.m */,
|
|
||||||
CD3F62AF297769F2004305D9 /* MBIMURLUtilities.h */,
|
|
||||||
CD3F62B0297769F2004305D9 /* MBIMURLUtilities.m */,
|
|
||||||
);
|
);
|
||||||
path = Utilities;
|
path = Utilities;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -538,32 +527,26 @@
|
|||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
1AA43E90219EBB3400EDF1A7 /* Utilities */,
|
1AA43E90219EBB3400EDF1A7 /* Utilities */,
|
||||||
CD2ECEC0269539100055E302 /* MBIMAuthenticateOperation.h */,
|
|
||||||
CD2ECEC1269539100055E302 /* MBIMAuthenticateOperation.m */,
|
|
||||||
CD602054219B5DFD0024D9C5 /* MBIMBridgeOperation.h */,
|
CD602054219B5DFD0024D9C5 /* MBIMBridgeOperation.h */,
|
||||||
CD602055219B5DFD0024D9C5 /* MBIMBridgeOperation.m */,
|
CD602055219B5DFD0024D9C5 /* MBIMBridgeOperation.m */,
|
||||||
|
CDD8C98226977D2A00551AE5 /* MBIMAliasValidationOperation.h */,
|
||||||
|
CDD8C98326977D2A00551AE5 /* MBIMAliasValidationOperation.m */,
|
||||||
|
CD2ECEC0269539100055E302 /* MBIMAuthenticateOperation.h */,
|
||||||
|
CD2ECEC1269539100055E302 /* MBIMAuthenticateOperation.m */,
|
||||||
|
CD60205A219B623F0024D9C5 /* MBIMMessagesListOperation.h */,
|
||||||
|
CD60205B219B623F0024D9C5 /* MBIMMessagesListOperation.m */,
|
||||||
CD60205D219B674B0024D9C5 /* MBIMConversationListOperation.h */,
|
CD60205D219B674B0024D9C5 /* MBIMConversationListOperation.h */,
|
||||||
CD60205E219B674B0024D9C5 /* MBIMConversationListOperation.m */,
|
CD60205E219B674B0024D9C5 /* MBIMConversationListOperation.m */,
|
||||||
CDDCF78B283F398C0087ABDF /* MBIMDeleteConversationOperation.h */,
|
|
||||||
CDDCF78C283F398C0087ABDF /* MBIMDeleteConversationOperation.m */,
|
|
||||||
CDE455A221A5308D0041F5DD /* MBIMFetchAttachmentOperation.h */,
|
CDE455A221A5308D0041F5DD /* MBIMFetchAttachmentOperation.h */,
|
||||||
CDE455A321A5308D0041F5DD /* MBIMFetchAttachmentOperation.m */,
|
CDE455A321A5308D0041F5DD /* MBIMFetchAttachmentOperation.m */,
|
||||||
CDE4559F21A365AD0041F5DD /* MBIMMarkOperation.h */,
|
CDE4559F21A365AD0041F5DD /* MBIMMarkOperation.h */,
|
||||||
CDE455A021A365AD0041F5DD /* MBIMMarkOperation.m */,
|
CDE455A021A365AD0041F5DD /* MBIMMarkOperation.m */,
|
||||||
CD60205A219B623F0024D9C5 /* MBIMMessagesListOperation.h */,
|
|
||||||
CD60205B219B623F0024D9C5 /* MBIMMessagesListOperation.m */,
|
|
||||||
CDEFF9FC2CACC7A700063C52 /* MBIMResolveHandleOperation.h */,
|
|
||||||
CDEFF9FD2CACC7A700063C52 /* MBIMResolveHandleOperation.m */,
|
|
||||||
CD602060219B68950024D9C5 /* MBIMSendMessageOperation.h */,
|
CD602060219B68950024D9C5 /* MBIMSendMessageOperation.h */,
|
||||||
CD602061219B68950024D9C5 /* MBIMSendMessageOperation.m */,
|
CD602061219B68950024D9C5 /* MBIMSendMessageOperation.m */,
|
||||||
CD936A37289B49FC0093A1AC /* MBIMStatusOperation.h */,
|
|
||||||
CD936A38289B49FC0093A1AC /* MBIMStatusOperation.m */,
|
|
||||||
CD14F19F219FE7D600E7DD22 /* MBIMUpdatePollOperation.h */,
|
CD14F19F219FE7D600E7DD22 /* MBIMUpdatePollOperation.h */,
|
||||||
CD14F1A0219FE7D600E7DD22 /* MBIMUpdatePollOperation.m */,
|
CD14F1A0219FE7D600E7DD22 /* MBIMUpdatePollOperation.m */,
|
||||||
1AD8936C21EFD986009B599A /* MBIMUploadAttachmentOperation.h */,
|
1AD8936C21EFD986009B599A /* MBIMUploadAttachmentOperation.h */,
|
||||||
1AD8936D21EFD986009B599A /* MBIMUploadAttachmentOperation.m */,
|
1AD8936D21EFD986009B599A /* MBIMUploadAttachmentOperation.m */,
|
||||||
CD936A33289B47D50093A1AC /* MBIMVersionOperation.h */,
|
|
||||||
CD936A34289B47D50093A1AC /* MBIMVersionOperation.m */,
|
|
||||||
);
|
);
|
||||||
path = Operations;
|
path = Operations;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -595,7 +578,6 @@
|
|||||||
1A0C446D219A4BCD00F2AC00 /* Bridge */,
|
1A0C446D219A4BCD00F2AC00 /* Bridge */,
|
||||||
CDF62334219A895D00690038 /* main.m */,
|
CDF62334219A895D00690038 /* main.m */,
|
||||||
1AAB32B221F835BD004A2A72 /* KPServer.pch */,
|
1AAB32B221F835BD004A2A72 /* KPServer.pch */,
|
||||||
CD936A2F289B31740093A1AC /* kordophoned-RestrictedEntitlements.plist */,
|
|
||||||
);
|
);
|
||||||
path = kordophone;
|
path = kordophone;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -603,6 +585,40 @@
|
|||||||
/* End PBXGroup section */
|
/* End PBXGroup section */
|
||||||
|
|
||||||
/* Begin PBXHeadersBuildPhase section */
|
/* Begin PBXHeadersBuildPhase section */
|
||||||
|
1ACFCDDF219EB28A00E2C237 /* Headers */ = {
|
||||||
|
isa = PBXHeadersBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
1ACFCFCB219EB2AC00E2C237 /* DDLog.h in Headers */,
|
||||||
|
1ACFCF40219EB2AC00E2C237 /* MultipartFormDataParser.h in Headers */,
|
||||||
|
1ACFCF2F219EB2AC00E2C237 /* HTTPAsyncFileResponse.h in Headers */,
|
||||||
|
1ACFCF37219EB2AC00E2C237 /* HTTPDataResponse.h in Headers */,
|
||||||
|
1ACFCF45219EB2AC00E2C237 /* MultipartMessageHeaderField.h in Headers */,
|
||||||
|
1ACFCF3A219EB2AC00E2C237 /* HTTPDynamicFileResponse.h in Headers */,
|
||||||
|
1ACFCFCC219EB2AC00E2C237 /* DDAbstractDatabaseLogger.h in Headers */,
|
||||||
|
1ACFCFD6219EB2AC00E2C237 /* DDTTYLogger.h in Headers */,
|
||||||
|
1ACFCF4C219EB2AC00E2C237 /* DDData.h in Headers */,
|
||||||
|
1ACFCF2D219EB2AC00E2C237 /* WebSocket.h in Headers */,
|
||||||
|
1ACFCF49219EB2AC00E2C237 /* DDRange.h in Headers */,
|
||||||
|
1ACFCF32219EB2AC00E2C237 /* HTTPRedirectResponse.h in Headers */,
|
||||||
|
1ACFCF2E219EB2AC00E2C237 /* HTTPAuthenticationRequest.h in Headers */,
|
||||||
|
1ACFCFDC219EB2AC00E2C237 /* GCDAsyncSocket.h in Headers */,
|
||||||
|
1ACFCF4A219EB2AC00E2C237 /* DDNumber.h in Headers */,
|
||||||
|
1ACFCF3F219EB2AC00E2C237 /* HTTPResponse.h in Headers */,
|
||||||
|
1ACFCF39219EB2AC00E2C237 /* HTTPFileResponse.h in Headers */,
|
||||||
|
1ACFCFCF219EB2AC00E2C237 /* ContextFilterLogFormatter.h in Headers */,
|
||||||
|
1ACFCF3B219EB2AC00E2C237 /* HTTPServer.h in Headers */,
|
||||||
|
1ACFCF41219EB2AC00E2C237 /* MultipartMessageHeader.h in Headers */,
|
||||||
|
1ACFCF38219EB2AC00E2C237 /* HTTPErrorResponse.h in Headers */,
|
||||||
|
1ACFCF2B219EB2AC00E2C237 /* HTTPLogging.h in Headers */,
|
||||||
|
1ACFCFD0219EB2AC00E2C237 /* DispatchQueueLogFormatter.h in Headers */,
|
||||||
|
1ACFCF2C219EB2AC00E2C237 /* HTTPMessage.h in Headers */,
|
||||||
|
1ACFCFD9219EB2AC00E2C237 /* DDASLLogger.h in Headers */,
|
||||||
|
1ACFCFCE219EB2AC00E2C237 /* DDFileLogger.h in Headers */,
|
||||||
|
1ACFCF2A219EB2AC00E2C237 /* HTTPConnection.h in Headers */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
CD83E15D219BE91500F4CCEA /* Headers */ = {
|
CD83E15D219BE91500F4CCEA /* Headers */ = {
|
||||||
isa = PBXHeadersBuildPhase;
|
isa = PBXHeadersBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
@@ -619,6 +635,8 @@
|
|||||||
buildPhases = (
|
buildPhases = (
|
||||||
1ACFCDDD219EB28A00E2C237 /* Sources */,
|
1ACFCDDD219EB28A00E2C237 /* Sources */,
|
||||||
1ACFCDDE219EB28A00E2C237 /* Frameworks */,
|
1ACFCDDE219EB28A00E2C237 /* Frameworks */,
|
||||||
|
1ACFCDDF219EB28A00E2C237 /* Headers */,
|
||||||
|
1ACFCDE0219EB28A00E2C237 /* Resources */,
|
||||||
);
|
);
|
||||||
buildRules = (
|
buildRules = (
|
||||||
);
|
);
|
||||||
@@ -626,8 +644,8 @@
|
|||||||
);
|
);
|
||||||
name = CocoaHTTPServer;
|
name = CocoaHTTPServer;
|
||||||
productName = CocoaHTTPServer;
|
productName = CocoaHTTPServer;
|
||||||
productReference = 1ACFCDE2219EB28A00E2C237 /* libCocoaHTTPServer.a */;
|
productReference = 1ACFCDE2219EB28A00E2C237 /* CocoaHTTPServer.framework */;
|
||||||
productType = "com.apple.product-type.library.static";
|
productType = "com.apple.product-type.framework";
|
||||||
};
|
};
|
||||||
CD14F18A219E2DB400E7DD22 /* Tests */ = {
|
CD14F18A219E2DB400E7DD22 /* Tests */ = {
|
||||||
isa = PBXNativeTarget;
|
isa = PBXNativeTarget;
|
||||||
@@ -668,10 +686,10 @@
|
|||||||
isa = PBXNativeTarget;
|
isa = PBXNativeTarget;
|
||||||
buildConfigurationList = CDF62336219A895D00690038 /* Build configuration list for PBXNativeTarget "kordophoned" */;
|
buildConfigurationList = CDF62336219A895D00690038 /* Build configuration list for PBXNativeTarget "kordophoned" */;
|
||||||
buildPhases = (
|
buildPhases = (
|
||||||
CD936A36289B48930093A1AC /* Compile Version String */,
|
|
||||||
CDF6232E219A895D00690038 /* Sources */,
|
CDF6232E219A895D00690038 /* Sources */,
|
||||||
CDF6232F219A895D00690038 /* Frameworks */,
|
CDF6232F219A895D00690038 /* Frameworks */,
|
||||||
CDF62330219A895D00690038 /* CopyFiles */,
|
CDF62330219A895D00690038 /* CopyFiles */,
|
||||||
|
1AAB32AD21F8245D004A2A72 /* Add current directory to rpath */,
|
||||||
);
|
);
|
||||||
buildRules = (
|
buildRules = (
|
||||||
);
|
);
|
||||||
@@ -729,6 +747,13 @@
|
|||||||
/* End PBXProject section */
|
/* End PBXProject section */
|
||||||
|
|
||||||
/* Begin PBXResourcesBuildPhase section */
|
/* Begin PBXResourcesBuildPhase section */
|
||||||
|
1ACFCDE0219EB28A00E2C237 /* Resources */ = {
|
||||||
|
isa = PBXResourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
CD14F189219E2DB400E7DD22 /* Resources */ = {
|
CD14F189219E2DB400E7DD22 /* Resources */ = {
|
||||||
isa = PBXResourcesBuildPhase;
|
isa = PBXResourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
@@ -739,7 +764,7 @@
|
|||||||
/* End PBXResourcesBuildPhase section */
|
/* End PBXResourcesBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXShellScriptBuildPhase section */
|
/* Begin PBXShellScriptBuildPhase section */
|
||||||
CD936A36289B48930093A1AC /* Compile Version String */ = {
|
1AAB32AD21F8245D004A2A72 /* Add current directory to rpath */ = {
|
||||||
isa = PBXShellScriptBuildPhase;
|
isa = PBXShellScriptBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
@@ -748,14 +773,14 @@
|
|||||||
);
|
);
|
||||||
inputPaths = (
|
inputPaths = (
|
||||||
);
|
);
|
||||||
name = "Compile Version String";
|
name = "Add current directory to rpath";
|
||||||
outputFileListPaths = (
|
outputFileListPaths = (
|
||||||
);
|
);
|
||||||
outputPaths = (
|
outputPaths = (
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
shellPath = /bin/sh;
|
shellPath = /bin/sh;
|
||||||
shellScript = "# Type a script or drag a script file from your workspace to insert its path.\nOUTPUT_FILE=\"$DERIVED_FILE_DIR/MBIMVersion.c\"\nVERSION_STR=`git describe`\n\necho \"const char* MBIMVersion() { return \\\"$VERSION_STR\\\"; }\" > $OUTPUT_FILE\n";
|
shellScript = "# Type a script or drag a script file from your workspace to insert its path.\n\nRESULT=$(install_name_tool -delete_rpath $BUILT_PRODUCTS_DIR $BUILT_PRODUCTS_DIR/$EXECUTABLE_NAME 2> /dev/null)\n\ninstall_name_tool -add_rpath $BUILT_PRODUCTS_DIR $BUILT_PRODUCTS_DIR/$EXECUTABLE_NAME\n";
|
||||||
};
|
};
|
||||||
/* End PBXShellScriptBuildPhase section */
|
/* End PBXShellScriptBuildPhase section */
|
||||||
|
|
||||||
@@ -812,7 +837,6 @@
|
|||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
CDA64B472DFCBF3000E9B07E /* MBIMPingPongWebSocket.m in Sources */,
|
|
||||||
CD14F1AD219FFAE100E7DD22 /* MBIMConcurrentHTTPServer.m in Sources */,
|
CD14F1AD219FFAE100E7DD22 /* MBIMConcurrentHTTPServer.m in Sources */,
|
||||||
1AA43E91219EBC2C00EDF1A7 /* MBIMHTTPConnection.m in Sources */,
|
1AA43E91219EBC2C00EDF1A7 /* MBIMHTTPConnection.m in Sources */,
|
||||||
CDF62339219A8A5600690038 /* MBIMBridge.h in Sources */,
|
CDF62339219A8A5600690038 /* MBIMBridge.h in Sources */,
|
||||||
@@ -821,27 +845,21 @@
|
|||||||
CD2ECEC526953F2A0055E302 /* MBIMAuthToken.m in Sources */,
|
CD2ECEC526953F2A0055E302 /* MBIMAuthToken.m in Sources */,
|
||||||
CD83E156219BE10A00F4CCEA /* hooking.m in Sources */,
|
CD83E156219BE10A00F4CCEA /* hooking.m in Sources */,
|
||||||
1AAB32B121F82EB7004A2A72 /* MBIMLogging.m in Sources */,
|
1AAB32B121F82EB7004A2A72 /* MBIMLogging.m in Sources */,
|
||||||
CDDCF78D283F398C0087ABDF /* MBIMDeleteConversationOperation.m in Sources */,
|
|
||||||
1AD8936E21EFD986009B599A /* MBIMUploadAttachmentOperation.m in Sources */,
|
1AD8936E21EFD986009B599A /* MBIMUploadAttachmentOperation.m in Sources */,
|
||||||
CDF6233A219A8A5600690038 /* MBIMBridge.m in Sources */,
|
CDF6233A219A8A5600690038 /* MBIMBridge.m in Sources */,
|
||||||
CDF62335219A895D00690038 /* main.m in Sources */,
|
CDF62335219A895D00690038 /* main.m in Sources */,
|
||||||
CD60205C219B623F0024D9C5 /* MBIMMessagesListOperation.m in Sources */,
|
CD60205C219B623F0024D9C5 /* MBIMMessagesListOperation.m in Sources */,
|
||||||
|
CDD8C98426977D2A00551AE5 /* MBIMAliasValidationOperation.m in Sources */,
|
||||||
CD14F1AA219FF3B800E7DD22 /* MBIMUpdateQueue.m in Sources */,
|
CD14F1AA219FF3B800E7DD22 /* MBIMUpdateQueue.m in Sources */,
|
||||||
CD3F62B1297769F2004305D9 /* MBIMURLUtilities.m in Sources */,
|
|
||||||
CD14F1A4219FF22700E7DD22 /* IMMessageItem+Encoded.m in Sources */,
|
CD14F1A4219FF22700E7DD22 /* IMMessageItem+Encoded.m in Sources */,
|
||||||
CD602062219B68950024D9C5 /* MBIMSendMessageOperation.m in Sources */,
|
CD602062219B68950024D9C5 /* MBIMSendMessageOperation.m in Sources */,
|
||||||
CD14F1A1219FE7D600E7DD22 /* MBIMUpdatePollOperation.m in Sources */,
|
CD14F1A1219FE7D600E7DD22 /* MBIMUpdatePollOperation.m in Sources */,
|
||||||
CD936A39289B49FC0093A1AC /* MBIMStatusOperation.m in Sources */,
|
|
||||||
CDEFF9FE2CACC7A700063C52 /* MBIMResolveHandleOperation.m in Sources */,
|
|
||||||
CDE455A121A365AD0041F5DD /* MBIMMarkOperation.m in Sources */,
|
CDE455A121A365AD0041F5DD /* MBIMMarkOperation.m in Sources */,
|
||||||
CDE455A721A531ED0041F5DD /* MBIMDataResponse.m in Sources */,
|
CDE455A721A531ED0041F5DD /* MBIMDataResponse.m in Sources */,
|
||||||
CD936A35289B47D60093A1AC /* MBIMVersionOperation.m in Sources */,
|
|
||||||
CD602056219B5DFD0024D9C5 /* MBIMBridgeOperation.m in Sources */,
|
CD602056219B5DFD0024D9C5 /* MBIMBridgeOperation.m in Sources */,
|
||||||
CD60205F219B674B0024D9C5 /* MBIMConversationListOperation.m in Sources */,
|
CD60205F219B674B0024D9C5 /* MBIMConversationListOperation.m in Sources */,
|
||||||
CDE4556421A3578A0041F5DD /* IMChat+Encoded.m in Sources */,
|
CDE4556421A3578A0041F5DD /* IMChat+Encoded.m in Sources */,
|
||||||
CD2782BF2952832B00C0C030 /* MBIMImageUtils.m in Sources */,
|
|
||||||
1AA43E8F219EBB2D00EDF1A7 /* MBIMJSONDataResponse.m in Sources */,
|
1AA43E8F219EBB2D00EDF1A7 /* MBIMJSONDataResponse.m in Sources */,
|
||||||
CD936A32289B353F0093A1AC /* MBIMErrorResponse.m in Sources */,
|
|
||||||
CD2ECEC2269539100055E302 /* MBIMAuthenticateOperation.m in Sources */,
|
CD2ECEC2269539100055E302 /* MBIMAuthenticateOperation.m in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
@@ -987,10 +1005,27 @@
|
|||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CODE_SIGN_IDENTITY = "";
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
DEFINES_MODULE = YES;
|
||||||
|
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||||
|
DYLIB_CURRENT_VERSION = 1;
|
||||||
|
DYLIB_INSTALL_NAME_BASE = "@rpath";
|
||||||
|
FRAMEWORK_VERSION = A;
|
||||||
|
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/../Frameworks",
|
||||||
|
"@loader_path/Frameworks",
|
||||||
|
);
|
||||||
MTL_ENABLE_DEBUG_INFO = YES;
|
MTL_ENABLE_DEBUG_INFO = YES;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.github.CocoaHTTPServer;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||||
SKIP_INSTALL = YES;
|
SKIP_INSTALL = YES;
|
||||||
|
VERSIONING_SYSTEM = "apple-generic";
|
||||||
|
VERSION_INFO_PREFIX = "";
|
||||||
};
|
};
|
||||||
name = Debug;
|
name = Debug;
|
||||||
};
|
};
|
||||||
@@ -998,9 +1033,26 @@
|
|||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CODE_SIGN_IDENTITY = "";
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
DEFINES_MODULE = YES;
|
||||||
|
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||||
|
DYLIB_CURRENT_VERSION = 1;
|
||||||
|
DYLIB_INSTALL_NAME_BASE = "@rpath";
|
||||||
|
FRAMEWORK_VERSION = A;
|
||||||
|
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/../Frameworks",
|
||||||
|
"@loader_path/Frameworks",
|
||||||
|
);
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.github.CocoaHTTPServer;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||||
SKIP_INSTALL = YES;
|
SKIP_INSTALL = YES;
|
||||||
|
VERSIONING_SYSTEM = "apple-generic";
|
||||||
|
VERSION_INFO_PREFIX = "";
|
||||||
};
|
};
|
||||||
name = Release;
|
name = Release;
|
||||||
};
|
};
|
||||||
@@ -1076,11 +1128,9 @@
|
|||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
CLANG_ENABLE_MODULES = NO;
|
CLANG_ENABLE_MODULES = NO;
|
||||||
CODE_SIGN_ENTITLEMENTS = "kordophone/kordophoned-RestrictedEntitlements.plist";
|
|
||||||
CODE_SIGN_IDENTITY = "-";
|
CODE_SIGN_IDENTITY = "-";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
GCC_PREFIX_HEADER = kordophone/KPServer.pch;
|
GCC_PREFIX_HEADER = kordophone/KPServer.pch;
|
||||||
OTHER_LDFLAGS = "-ObjC";
|
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SDKROOT = macosx;
|
SDKROOT = macosx;
|
||||||
SYSTEM_FRAMEWORK_SEARCH_PATHS = (
|
SYSTEM_FRAMEWORK_SEARCH_PATHS = (
|
||||||
@@ -1097,7 +1147,6 @@
|
|||||||
CODE_SIGN_IDENTITY = "-";
|
CODE_SIGN_IDENTITY = "-";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
GCC_PREFIX_HEADER = kordophone/KPServer.pch;
|
GCC_PREFIX_HEADER = kordophone/KPServer.pch;
|
||||||
OTHER_LDFLAGS = "-ObjC";
|
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SDKROOT = macosx;
|
SDKROOT = macosx;
|
||||||
SYSTEM_FRAMEWORK_SEARCH_PATHS = (
|
SYSTEM_FRAMEWORK_SEARCH_PATHS = (
|
||||||
99
README.md
@@ -1,66 +1,53 @@
|
|||||||
# Kordophone Monorepo
|
# Entitlements
|
||||||
|
|
||||||
Kordophone is an iMessage bridge: a lightweight server runs on a Mac and exposes an HTTP API so non‑Apple devices can send/receive iMessages. A set of clients (Android, Linux GTK, macOS Cocoa) talk to that API. A shared Rust library powers most clients, and a mock server helps local testing.
|
You might to enable this default to use private entitlements
|
||||||
|
```
|
||||||
> Important: Interfacing with iMessage on macOS involves private APIs and restricted entitlements. See `server/README.md` for details and safety notes.
|
sudo defaults write /Library/Preferences/com.apple.security.coderequirements Entitlements -string always
|
||||||
|
|
||||||
## Repository Layout
|
|
||||||
|
|
||||||
Top‑level projects in this monorepo:
|
|
||||||
|
|
||||||
- `server/` — macOS daemon that bridges to iMessage and exposes an HTTP REST API. Written in Objective‑C/Cocoa. See `server/README.md`.
|
|
||||||
- `core/` — Rust workspace with the shared Kordophone client library and related tooling. Used by `gtk/` and `osx/`.
|
|
||||||
- `gtk/` — GTK4/Libadwaita Linux desktop client built on the Rust `core` library.
|
|
||||||
- `osx/` — macOS Cocoa client that uses the Rust `core` library and talks to the local Kordophone client daemon via XPC.
|
|
||||||
- `android/` — Android client. Currently implements its own API client (does not use the Rust library yet).
|
|
||||||
- `mock/` — Go‑based mock server that emulates a Mac running the Kordophone server, for local development and tests.
|
|
||||||
|
|
||||||
Quick links:
|
|
||||||
|
|
||||||
- Server (mac daemon): `server/`
|
|
||||||
- Core (Rust workspace): `core/`
|
|
||||||
- GTK client (Linux): `gtk/`
|
|
||||||
- macOS client (Cocoa): `osx/`
|
|
||||||
- Android client: `android/`
|
|
||||||
- Mock server (Go): `mock/`
|
|
||||||
|
|
||||||
## How It Works
|
|
||||||
|
|
||||||
1. The macOS Kordophone Server (`server/`) runs on a Mac with iMessage and exposes an HTTP/JSON API and a WebSocket/updates channel.
|
|
||||||
2. Clients connect to that server to list conversations, fetch/send messages, upload/download attachments, and receive live updates.
|
|
||||||
3. A Rust library in `core/kordophone` implements the wire protocol and client behaviors. Linux/macOS clients use this library directly; Android currently ships a native Kotlin client.
|
|
||||||
4. The Rust client daemon (`core/kordophoned`) provides local caching and IPC (D‑Bus on Linux, XPC on macOS) for GUI apps.
|
|
||||||
5. The `mock/` server simulates the server API for local development without a Mac.
|
|
||||||
|
|
||||||
## Getting Started
|
|
||||||
|
|
||||||
You can try the clients quickly against the mock server.
|
|
||||||
|
|
||||||
1) Run the mock server (no auth):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd mock
|
|
||||||
go run ./... # or: make; ./kordophone-mock
|
|
||||||
```
|
```
|
||||||
|
|
||||||
The mock server listens on `http://localhost:5738`.
|
Maybe a better thing to do is to DYLD_PRELOAD `imagent` and swizzle `IMDAuditTokenTaskHasEntitlement` to always return YES.
|
||||||
|
|
||||||
2) Point a client at the mock server:
|
|
||||||
|
|
||||||
- Android: open Settings in the app and set the server host to `http://10.0.2.2:5738` (Android emulator) or `http://<your-host-ip>:5738` if running on device. Disable auth unless you started the mock with `--auth`.
|
## Building/linking
|
||||||
- GTK (Linux): build and run the GTK app from `gtk/` (see its README) and configure it to use the local daemon backed by the server URL.
|
If you get dyld errors running from the command line, use `install_name_tool` to update the @rpath (where @rpath points to where linked Frameworks like GCDWebServer is).
|
||||||
- macOS (Cocoa): build the app in `osx/` (see `osx/README.md`).
|
`install_name_tool -add_rpath . ./kordophoned`
|
||||||
|
|
||||||
To use a real Mac server instead, set your client’s server URL to the host running `server/` with the correct scheme/port and authentication.
|
|
||||||
|
|
||||||
## Building
|
## Running
|
||||||
|
You need to hook imagent first to bypass entitlements check. Look at `hookAgent.sh`
|
||||||
|
|
||||||
Below are brief notes. Each subproject’s README has more detail.
|
|
||||||
|
|
||||||
- Server (macOS): open `server/MessagesBridge.xcodeproj` in Xcode. See `server/README.md` for entitlements, SSL, and running.
|
## SSL
|
||||||
- Core (Rust): install Rust (stable) and run `cargo build` inside `core/`. See `core/README.md`.
|
If you want to run with SSL, you have to generate a self-signed certificate, and have the Mac trust the root cert.
|
||||||
- GTK (Linux): see `gtk/README.md` for RPM build via `rpmbuild -ba dist/rpm/kordophone.spec`.
|
|
||||||
- macOS (Cocoa): open `osx/kordophone2.xcodeproj` in Xcode. See `osx/README.md`.
|
### Generate a root cert
|
||||||
- Android: open `android/` in Android Studio and build. See `android/README.md` for configuration.
|
1. Generate root key
|
||||||
- Mock server (Go): `cd mock && go run ./...` or `make`.
|
`openssl genrsa -out Kordophone-root.key 4096`
|
||||||
|
2. Generate root certificate
|
||||||
|
`openssl req -x509 -new -nodes -key Kordophone-root.key -sha256 -days 1024 -out Kordophone-root.crt`
|
||||||
|
3. Add this certificate to the Mac's trust store via Keychain Access. Set to "Always Trust"
|
||||||
|
|
||||||
|
### Create signing certificate by signing a new cert with the root cert
|
||||||
|
1. Generate signing key
|
||||||
|
`openssl genrsa -out kp.localhost.key 2048`
|
||||||
|
2. Create certificate signing request
|
||||||
|
`openssl req -new -key kp.localhost.key -out kp.localhost.csr`
|
||||||
|
3. Sign the cert with the root cert
|
||||||
|
`openssl x509 -req -in kp.localhost.csr -CA Kordophone-root.crt -CAkey Kordophone-root.key -CAcreateserial -out kp.localhost.crt -days 365 -sha256`
|
||||||
|
4. kordophoned works with a signing cert in PKCS12 format. Convert the cert and the privkey to PKCS12
|
||||||
|
`openssl pkcs12 -export -in kp.localhost.crt -inkey kp.localhost.key -out certificate.p12 -name "Kordophone"`
|
||||||
|
|
||||||
|
### Start kordophone with the SSL options and provide the p12
|
||||||
|
`kordophoned -s -c certificate.p12`
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
Basic Authentication is also optional, but requires SSL to be enabled as well. To configure basic authentication, create a file containing your username and password on two separate lines encrypted with your GPG key.
|
||||||
|
|
||||||
|
`echo "username\npassword" > password.txt"`
|
||||||
|
`gpg -e -r (your email) -o password.asc password.txt`
|
||||||
|
|
||||||
|
Then run kordophoned with the following option
|
||||||
|
`kordophone -s -c certificate.p12 -a password.asc`
|
||||||
|
|
||||||
|
You may need to unlock your GPG keyring (via gpg-agent) when running kordophoned the first time.
|
||||||
|
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
image: ubuntu/jammy
|
|
||||||
packages:
|
|
||||||
- openjdk-18-jdk
|
|
||||||
- gradle
|
|
||||||
- maven
|
|
||||||
sources:
|
|
||||||
- https://git.sr.ht/~buzzert/KordophoneDroid
|
|
||||||
secrets:
|
|
||||||
- a24d65d9-3e71-40e9-946d-0e9b73efacee # ~/.gradle/gradle.properties: contains keystore passwords
|
|
||||||
- 4fbe9d83-5f38-49c0-b93d-863d15e92a60 # ~/keystore.jks: Android keystore
|
|
||||||
tasks:
|
|
||||||
- setup: |
|
|
||||||
wget https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip
|
|
||||||
unzip commandlinetools-linux-11076708_latest.zip
|
|
||||||
mkdir android-sdk
|
|
||||||
yes | ./cmdline-tools/bin/sdkmanager --sdk_root=android-sdk --licenses
|
|
||||||
./cmdline-tools/bin/sdkmanager --sdk_root=android-sdk "build-tools;34.0.0" "platforms;android-33"
|
|
||||||
- build: |
|
|
||||||
export ANDROID_HOME=~/android-sdk
|
|
||||||
cd KordophoneDroid/
|
|
||||||
./gradlew assembleRelease
|
|
||||||
- prepare: |
|
|
||||||
cd KordophoneDroid/app/build/outputs/apk/release/
|
|
||||||
cp app-arm64-v8a-release.apk ~/kordophone-arm64-v8a-release.apk
|
|
||||||
cp app-armeabi-v7a-release.apk ~/kordophone-armeabi-v7a-release.apk
|
|
||||||
cp app-x86_64-release.apk ~/kordophone-x86_64-release.apk
|
|
||||||
cp app-x86-release.apk ~/kordophone-x86-release.apk
|
|
||||||
artifacts:
|
|
||||||
- kordophone-arm64-v8a-release.apk
|
|
||||||
- kordophone-armeabi-v7a-release.apk
|
|
||||||
- kordophone-x86_64-release.apk
|
|
||||||
- kordophone-x86-release.apk
|
|
||||||
17
android/.gitignore
vendored
@@ -1,17 +0,0 @@
|
|||||||
*.iml
|
|
||||||
.gradle
|
|
||||||
/local.properties
|
|
||||||
/.idea/caches
|
|
||||||
/.idea/libraries
|
|
||||||
/.idea/modules.xml
|
|
||||||
/.idea/workspace.xml
|
|
||||||
/.idea/navEditor.xml
|
|
||||||
/.idea/assetWizardSettings.xml
|
|
||||||
.DS_Store
|
|
||||||
/build
|
|
||||||
/captures
|
|
||||||
.externalNativeBuild
|
|
||||||
.cxx
|
|
||||||
local.properties
|
|
||||||
|
|
||||||
.idea/
|
|
||||||
3
android/.idea/.gitignore
generated
vendored
@@ -1,3 +0,0 @@
|
|||||||
# Default ignored files
|
|
||||||
/shelf/
|
|
||||||
/workspace.xml
|
|
||||||
6
android/.idea/compiler.xml
generated
@@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="CompilerConfiguration">
|
|
||||||
<bytecodeTargetLevel target="1.8" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
20
android/.idea/gradle.xml
generated
@@ -1,20 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="GradleMigrationSettings" migrationVersion="1" />
|
|
||||||
<component name="GradleSettings">
|
|
||||||
<option name="linkedExternalProjectsSettings">
|
|
||||||
<GradleProjectSettings>
|
|
||||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
|
||||||
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
|
|
||||||
<option name="modules">
|
|
||||||
<set>
|
|
||||||
<option value="$PROJECT_DIR$" />
|
|
||||||
<option value="$PROJECT_DIR$/app" />
|
|
||||||
<option value="$PROJECT_DIR$/backend" />
|
|
||||||
</set>
|
|
||||||
</option>
|
|
||||||
<option name="resolveExternalAnnotations" value="false" />
|
|
||||||
</GradleProjectSettings>
|
|
||||||
</option>
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
41
android/.idea/inspectionProfiles/Project_Default.xml
generated
@@ -1,41 +0,0 @@
|
|||||||
<component name="InspectionProjectProfileManager">
|
|
||||||
<profile version="1.0">
|
|
||||||
<option name="myName" value="Project Default" />
|
|
||||||
<inspection_tool class="PreviewAnnotationInFunctionWithParameters" enabled="true" level="ERROR" enabled_by_default="true">
|
|
||||||
<option name="composableFile" value="true" />
|
|
||||||
<option name="previewFile" value="true" />
|
|
||||||
</inspection_tool>
|
|
||||||
<inspection_tool class="PreviewApiLevelMustBeValid" enabled="true" level="ERROR" enabled_by_default="true">
|
|
||||||
<option name="composableFile" value="true" />
|
|
||||||
<option name="previewFile" value="true" />
|
|
||||||
</inspection_tool>
|
|
||||||
<inspection_tool class="PreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
|
|
||||||
<option name="composableFile" value="true" />
|
|
||||||
<option name="previewFile" value="true" />
|
|
||||||
</inspection_tool>
|
|
||||||
<inspection_tool class="PreviewFontScaleMustBeGreaterThanZero" enabled="true" level="ERROR" enabled_by_default="true">
|
|
||||||
<option name="composableFile" value="true" />
|
|
||||||
<option name="previewFile" value="true" />
|
|
||||||
</inspection_tool>
|
|
||||||
<inspection_tool class="PreviewMultipleParameterProviders" enabled="true" level="ERROR" enabled_by_default="true">
|
|
||||||
<option name="composableFile" value="true" />
|
|
||||||
<option name="previewFile" value="true" />
|
|
||||||
</inspection_tool>
|
|
||||||
<inspection_tool class="PreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
|
|
||||||
<option name="composableFile" value="true" />
|
|
||||||
<option name="previewFile" value="true" />
|
|
||||||
</inspection_tool>
|
|
||||||
<inspection_tool class="PreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
|
|
||||||
<option name="composableFile" value="true" />
|
|
||||||
<option name="previewFile" value="true" />
|
|
||||||
</inspection_tool>
|
|
||||||
<inspection_tool class="PreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
|
|
||||||
<option name="composableFile" value="true" />
|
|
||||||
<option name="previewFile" value="true" />
|
|
||||||
</inspection_tool>
|
|
||||||
<inspection_tool class="PreviewPickerAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
|
|
||||||
<option name="composableFile" value="true" />
|
|
||||||
<option name="previewFile" value="true" />
|
|
||||||
</inspection_tool>
|
|
||||||
</profile>
|
|
||||||
</component>
|
|
||||||
6
android/.idea/kotlinc.xml
generated
@@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="KotlinJpsPluginSettings">
|
|
||||||
<option name="version" value="1.8.22" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
9
android/.idea/misc.xml
generated
@@ -1,9 +0,0 @@
|
|||||||
<project version="4">
|
|
||||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
|
||||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" project-jdk-name="jbr-17" project-jdk-type="JavaSDK">
|
|
||||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
|
||||||
</component>
|
|
||||||
<component name="ProjectType">
|
|
||||||
<option name="id" value="Android" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
7
android/.idea/vcs.xml
generated
@@ -1,7 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="VcsDirectoryMappings">
|
|
||||||
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
|
|
||||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
# Kordophone Android Client
|
|
||||||
|
|
||||||
Android client for the Kordophone iMessage bridge. This app connects to a running Kordophone server over HTTP/JSON and streams updates.
|
|
||||||
|
|
||||||
Note: This client currently implements its own API layer in Kotlin and does not yet use the shared Rust `core` library.
|
|
||||||
|
|
||||||
## Build & Run
|
|
||||||
|
|
||||||
Requirements:
|
|
||||||
|
|
||||||
- Android Studio (AGP 8.x)
|
|
||||||
- JDK 8+ toolchain (project uses Java 8 bytecode)
|
|
||||||
|
|
||||||
Steps:
|
|
||||||
|
|
||||||
1. Open `android/` in Android Studio.
|
|
||||||
2. Sync Gradle and build the project.
|
|
||||||
3. Run the `app` configuration on an emulator or device.
|
|
||||||
|
|
||||||
The app targets `minSdk 30`, `targetSdk 33` and uses Jetpack Compose.
|
|
||||||
|
|
||||||
## Configure Server
|
|
||||||
|
|
||||||
Set the server host and optional Basic Auth from the app’s Settings screen.
|
|
||||||
|
|
||||||
- Server URL for local mock (emulator): `http://10.0.2.2:5738`
|
|
||||||
- Server URL for local mock (device): `http://<your-host-ip>:5738`
|
|
||||||
- Credentials: match the server if auth is enabled (mock supports `--auth`).
|
|
||||||
|
|
||||||
Settings are persisted using `EncryptedSharedPreferences`.
|
|
||||||
|
|
||||||
## Modules
|
|
||||||
|
|
||||||
- `app/` — UI (Compose), DI (Hilt), app wiring
|
|
||||||
- `backend/` — Kotlin API client, DB/cache (Realm), models, tests
|
|
||||||
|
|
||||||
## Testing with the Mock Server
|
|
||||||
|
|
||||||
Start the mock server in the repo root:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd mock
|
|
||||||
go run ./... # or: make; ./kordophone-mock
|
|
||||||
```
|
|
||||||
|
|
||||||
Set the app’s server URL to the mock address and run. The mock implements the Kordophone server API; see centralized API documentation (planned under `api/`).
|
|
||||||
2
android/app/.gitignore
vendored
@@ -1,2 +0,0 @@
|
|||||||
/build
|
|
||||||
/release
|
|
||||||
@@ -1,155 +0,0 @@
|
|||||||
plugins {
|
|
||||||
id 'com.android.application'
|
|
||||||
id 'org.jetbrains.kotlin.android'
|
|
||||||
id 'kotlin-kapt'
|
|
||||||
id 'com.google.dagger.hilt.android'
|
|
||||||
}
|
|
||||||
|
|
||||||
android {
|
|
||||||
namespace 'net.buzzert.kordophonedroid'
|
|
||||||
compileSdk 33
|
|
||||||
|
|
||||||
defaultConfig {
|
|
||||||
applicationId "net.buzzert.kordophonedroid"
|
|
||||||
minSdk 30
|
|
||||||
targetSdk 33
|
|
||||||
versionCode 1
|
|
||||||
versionName "1.0"
|
|
||||||
|
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
|
||||||
vectorDrawables {
|
|
||||||
useSupportLibrary true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
signingConfigs {
|
|
||||||
if (project.hasProperty('RELEASE_STORE_FILE')) {
|
|
||||||
release {
|
|
||||||
storeFile file(RELEASE_STORE_FILE)
|
|
||||||
storePassword RELEASE_STORE_PASSWORD
|
|
||||||
keyAlias RELEASE_KEY_ALIAS
|
|
||||||
keyPassword RELEASE_KEY_PASSWORD
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
buildTypes {
|
|
||||||
release {
|
|
||||||
minifyEnabled false
|
|
||||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
|
||||||
|
|
||||||
if (project.hasProperty('RELEASE_STORE_FILE')) {
|
|
||||||
signingConfig signingConfigs.release
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
compileOptions {
|
|
||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
|
||||||
targetCompatibility JavaVersion.VERSION_1_8
|
|
||||||
}
|
|
||||||
|
|
||||||
kotlinOptions {
|
|
||||||
jvmTarget = JavaVersion.VERSION_1_8.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
buildFeatures {
|
|
||||||
compose true
|
|
||||||
}
|
|
||||||
|
|
||||||
composeOptions {
|
|
||||||
// Note: this is strictly tied to a kotlin version, but isn't the version of kotlin exactly.
|
|
||||||
// See: https://developer.android.com/jetpack/androidx/releases/compose-kotlin
|
|
||||||
kotlinCompilerExtensionVersion '1.4.8'
|
|
||||||
}
|
|
||||||
|
|
||||||
packagingOptions {
|
|
||||||
resources {
|
|
||||||
excludes += '/META-INF/{AL2.0,LGPL2.1}'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
splits {
|
|
||||||
abi {
|
|
||||||
// Enable building for multiple ABIs
|
|
||||||
enable true
|
|
||||||
|
|
||||||
// Include x86/x86_64 APKs
|
|
||||||
include "x86", "x86_64"
|
|
||||||
universalApk false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
buildToolsVersion '33.0.1'
|
|
||||||
}
|
|
||||||
|
|
||||||
kotlin {
|
|
||||||
jvmToolchain(8)
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
implementation "androidx.compose.material3:material3:1.1.1"
|
|
||||||
implementation "androidx.core:core-ktx:${kotlin_version}"
|
|
||||||
|
|
||||||
// Kordophone lib
|
|
||||||
implementation project(':backend')
|
|
||||||
implementation 'androidx.security:security-crypto-ktx:1.1.0-alpha06'
|
|
||||||
|
|
||||||
// Navigation
|
|
||||||
def nav_version = "2.6.0"
|
|
||||||
|
|
||||||
// Java language implementation
|
|
||||||
implementation "androidx.navigation:navigation-fragment:$nav_version"
|
|
||||||
implementation "androidx.navigation:navigation-ui:$nav_version"
|
|
||||||
|
|
||||||
// Kotlin
|
|
||||||
implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
|
|
||||||
implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
|
|
||||||
|
|
||||||
// Feature module Support
|
|
||||||
implementation "androidx.navigation:navigation-dynamic-features-fragment:$nav_version"
|
|
||||||
|
|
||||||
// Testing Navigation
|
|
||||||
androidTestImplementation "androidx.navigation:navigation-testing:$nav_version"
|
|
||||||
|
|
||||||
// Jetpack Compose Integration
|
|
||||||
implementation "androidx.navigation:navigation-compose:$nav_version"
|
|
||||||
|
|
||||||
// Jetpack Compose
|
|
||||||
def compose_version = "1.4.3"
|
|
||||||
implementation "androidx.compose.ui:ui:$compose_version"
|
|
||||||
implementation "androidx.compose.material:material:$compose_version"
|
|
||||||
implementation "androidx.compose.foundation:foundation:$compose_version"
|
|
||||||
|
|
||||||
|
|
||||||
implementation "androidx.activity:activity-compose:$compose_version"
|
|
||||||
|
|
||||||
// Lifecycle
|
|
||||||
def lifecycle_version = "2.6.1"
|
|
||||||
implementation "androidx.lifecycle:lifecycle-runtime-compose:$lifecycle_version"
|
|
||||||
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"
|
|
||||||
implementation "androidx.lifecycle:lifecycle-viewmodel-compose:$lifecycle_version"
|
|
||||||
|
|
||||||
|
|
||||||
// Hilt (dependency injection)
|
|
||||||
implementation "com.google.dagger:hilt-android:${hilt_version}"
|
|
||||||
implementation "androidx.hilt:hilt-navigation-compose:1.0.0"
|
|
||||||
kapt "com.google.dagger:hilt-compiler:${hilt_version}"
|
|
||||||
|
|
||||||
// Coil (image loading library)
|
|
||||||
implementation "io.coil-kt:coil:2.4.0"
|
|
||||||
implementation "io.coil-kt:coil-compose:2.4.0"
|
|
||||||
|
|
||||||
// Disk LRU Cache
|
|
||||||
implementation "com.jakewharton:disklrucache:2.0.2"
|
|
||||||
|
|
||||||
// Zooming in images
|
|
||||||
implementation "net.engawapg.lib:zoomable:$compose_version"
|
|
||||||
|
|
||||||
debugImplementation "androidx.compose.ui:ui-tooling:$compose_version"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Allow references to generated code
|
|
||||||
kapt {
|
|
||||||
correctErrorTypes true
|
|
||||||
}
|
|
||||||
21
android/app/proguard-rules.pro
vendored
@@ -1,21 +0,0 @@
|
|||||||
# Add project specific ProGuard rules here.
|
|
||||||
# You can control the set of applied configuration files using the
|
|
||||||
# proguardFiles setting in build.gradle.
|
|
||||||
#
|
|
||||||
# For more details, see
|
|
||||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
|
||||||
|
|
||||||
# If your project uses WebView with JS, uncomment the following
|
|
||||||
# and specify the fully qualified class name to the JavaScript interface
|
|
||||||
# class:
|
|
||||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
|
||||||
# public *;
|
|
||||||
#}
|
|
||||||
|
|
||||||
# Uncomment this to preserve the line number information for
|
|
||||||
# debugging stack traces.
|
|
||||||
#-keepattributes SourceFile,LineNumberTable
|
|
||||||
|
|
||||||
# If you keep the line number information, uncomment this to
|
|
||||||
# hide the original source file name.
|
|
||||||
#-renamesourcefileattribute SourceFile
|
|
||||||
@@ -1,137 +0,0 @@
|
|||||||
package previews
|
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.material.MaterialTheme
|
|
||||||
import androidx.compose.material.Scaffold
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.res.painterResource
|
|
||||||
import androidx.compose.ui.text.input.TextFieldValue
|
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import net.buzzert.kordophonedroid.R
|
|
||||||
import net.buzzert.kordophonedroid.ui.conversationlist.ConversationListItem
|
|
||||||
import net.buzzert.kordophonedroid.ui.conversationlist.ConversationListScreen
|
|
||||||
import net.buzzert.kordophonedroid.ui.conversationlist.NoContentView
|
|
||||||
import net.buzzert.kordophonedroid.ui.messagelist.AttachmentRowItem
|
|
||||||
import net.buzzert.kordophonedroid.ui.messagelist.MessageEntry
|
|
||||||
import net.buzzert.kordophonedroid.ui.messagelist.MessageListItem
|
|
||||||
import net.buzzert.kordophonedroid.ui.messagelist.MessageMetadata
|
|
||||||
import net.buzzert.kordophonedroid.ui.messagelist.MessageTranscript
|
|
||||||
import net.buzzert.kordophonedroid.ui.settings.SettingsScreen
|
|
||||||
import java.util.Date
|
|
||||||
|
|
||||||
// - Conversation List
|
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
|
||||||
fun ConversationListItemPreview() {
|
|
||||||
Column(modifier = Modifier.background(MaterialTheme.colors.background)) {
|
|
||||||
ConversationListItem(name = "James Magahern", id = "asdf", lastMessagePreview = "This is a test", date = Date(), isUnread = true) {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
|
||||||
fun ConversationListScreenPreview() {
|
|
||||||
ConversationListScreen()
|
|
||||||
}
|
|
||||||
|
|
||||||
// - Message List
|
|
||||||
|
|
||||||
private fun testMessageMetadata(fromMe: Boolean, delivered: Boolean): MessageMetadata {
|
|
||||||
return MessageMetadata(
|
|
||||||
fromMe = fromMe,
|
|
||||||
fromAddress = if (fromMe) "<me>" else "cool@cool.com",
|
|
||||||
date = Date(),
|
|
||||||
delivered = delivered,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun makeTestTextMessageItem(text: String, fromMe: Boolean, delivered: Boolean = true): MessageListItem {
|
|
||||||
return MessageListItem.TextMessage(
|
|
||||||
text = text,
|
|
||||||
metadata = testMessageMetadata(fromMe = fromMe, delivered = delivered)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun makeTestImageMessageItem(fromMe: Boolean, delivered: Boolean = true): MessageListItem {
|
|
||||||
return MessageListItem.ImageAttachmentMessage(
|
|
||||||
guid = "asdf",
|
|
||||||
metadata = testMessageMetadata(fromMe, delivered)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
|
||||||
private fun MessageListScreenPreview() {
|
|
||||||
val messages = listOf<MessageListItem>(
|
|
||||||
makeTestImageMessageItem(false),
|
|
||||||
|
|
||||||
makeTestTextMessageItem("Hello", false),
|
|
||||||
makeTestTextMessageItem( "Hey there, this is a longer text message that might wrap to another line", true),
|
|
||||||
makeTestTextMessageItem("How's it going", fromMe = true, delivered = false)
|
|
||||||
).reversed()
|
|
||||||
|
|
||||||
Scaffold() {
|
|
||||||
MessageTranscript(
|
|
||||||
messages = messages,
|
|
||||||
paddingValues = it,
|
|
||||||
showSenders = true,
|
|
||||||
attachmentUris = setOf(),
|
|
||||||
onAddAttachment = {},
|
|
||||||
onClearAttachments = {},
|
|
||||||
onSendMessage = {}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Preview(showBackground = true)
|
|
||||||
@Composable
|
|
||||||
private fun MessageEntryPreview() {
|
|
||||||
var textState by rememberSaveable(stateSaver = TextFieldValue.Saver) {
|
|
||||||
mutableStateOf(TextFieldValue("Hello this is some text that might wrap multiple lines to show that there must be some padding here. "))
|
|
||||||
}
|
|
||||||
|
|
||||||
MessageEntry(onSend = {}, onTextChanged = {}, textFieldValue = textState)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Preview(showBackground = true)
|
|
||||||
@Composable
|
|
||||||
private fun MessageEntryWithAttachmentsPreview() {
|
|
||||||
var textState by rememberSaveable(stateSaver = TextFieldValue.Saver) {
|
|
||||||
mutableStateOf(TextFieldValue("Attachments"))
|
|
||||||
}
|
|
||||||
|
|
||||||
MessageEntry(onSend = {}, onTextChanged = {}, textFieldValue = textState, attachmentItems = listOf(
|
|
||||||
AttachmentRowItem(painterResource(id = R.drawable.sedona), "id")
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
// - No content
|
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
|
||||||
fun NoContentPreview() {
|
|
||||||
Scaffold {
|
|
||||||
NoContentView(
|
|
||||||
icon = R.drawable.storage,
|
|
||||||
text = "Server not configured",
|
|
||||||
onSettings = {},
|
|
||||||
modifier = Modifier.padding(it)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// - Settings
|
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
|
||||||
fun SettingsPreview() {
|
|
||||||
SettingsScreen()
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
|
||||||
|
|
||||||
<application
|
|
||||||
android:allowBackup="true"
|
|
||||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
|
||||||
android:fullBackupContent="@xml/backup_rules"
|
|
||||||
android:icon="@mipmap/ic_launcher"
|
|
||||||
android:label="@string/app_name"
|
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
|
||||||
android:supportsRtl="true"
|
|
||||||
android:theme="@style/Theme.KordophoneDroid"
|
|
||||||
android:name=".KordophoneApplication"
|
|
||||||
android:networkSecurityConfig="@xml/network_security_config"
|
|
||||||
android:usesCleartextTraffic="true"
|
|
||||||
>
|
|
||||||
<activity
|
|
||||||
android:name=".MainActivity"
|
|
||||||
android:exported="true"
|
|
||||||
android:label="@string/app_name"
|
|
||||||
android:windowSoftInputMode="adjustResize"
|
|
||||||
|
|
||||||
android:theme="@style/Theme.KordophoneDroid">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.MAIN" />
|
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
|
||||||
</intent-filter>
|
|
||||||
|
|
||||||
<intent-filter>
|
|
||||||
<!-- Message List Deep Link -->
|
|
||||||
<data android:scheme="kordophone" android:host="messages" />
|
|
||||||
</intent-filter>
|
|
||||||
|
|
||||||
<meta-data
|
|
||||||
android:name="android.app.lib_name"
|
|
||||||
android:value="" />
|
|
||||||
</activity>
|
|
||||||
|
|
||||||
<service
|
|
||||||
android:name=".UpdateMonitorService"
|
|
||||||
android:foregroundServiceType="dataSync"
|
|
||||||
android:exported="false" />
|
|
||||||
</application>
|
|
||||||
|
|
||||||
</manifest>
|
|
||||||
|
Before Width: | Height: | Size: 33 KiB |
@@ -1,41 +0,0 @@
|
|||||||
package net.buzzert.kordophonedroid
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import dagger.Module
|
|
||||||
import dagger.Provides
|
|
||||||
import dagger.hilt.InstallIn
|
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
|
||||||
import dagger.hilt.components.SingletonComponent
|
|
||||||
import net.buzzert.kordophone.backend.db.CachedChatDatabase
|
|
||||||
import net.buzzert.kordophone.backend.server.APIClientFactory
|
|
||||||
import net.buzzert.kordophone.backend.server.ChatRepository
|
|
||||||
import net.buzzert.kordophonedroid.ui.attachments.AttachmentImageLoader
|
|
||||||
import net.buzzert.kordophonedroid.ui.shared.ServerConfigRepository
|
|
||||||
import javax.inject.Singleton
|
|
||||||
|
|
||||||
@Module
|
|
||||||
@InstallIn(SingletonComponent::class)
|
|
||||||
object AppModule {
|
|
||||||
@Singleton
|
|
||||||
@Provides
|
|
||||||
fun provideChatRepository(configRepository: ServerConfigRepository): ChatRepository {
|
|
||||||
val serverConfig = configRepository.serverConfig.value
|
|
||||||
|
|
||||||
val server = serverConfig.serverName
|
|
||||||
val authentication = serverConfig.authentication?.toBackendAuthentication()
|
|
||||||
val client = APIClientFactory.createClient(server, authentication)
|
|
||||||
|
|
||||||
val database = CachedChatDatabase.liveDatabase()
|
|
||||||
return ChatRepository(client, database)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Singleton
|
|
||||||
@Provides
|
|
||||||
fun provideAttachmentFactory(
|
|
||||||
chatRepository: ChatRepository,
|
|
||||||
@ApplicationContext context: Context
|
|
||||||
): AttachmentImageLoader
|
|
||||||
{
|
|
||||||
return AttachmentImageLoader(chatRepository, context)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
package net.buzzert.kordophonedroid
|
|
||||||
|
|
||||||
import android.app.Application
|
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
|
||||||
import dagger.hilt.android.HiltAndroidApp
|
|
||||||
import net.buzzert.kordophonedroid.data.AppContainer
|
|
||||||
|
|
||||||
@HiltAndroidApp
|
|
||||||
class KordophoneApplication : Application() {
|
|
||||||
override fun onCreate() {
|
|
||||||
super.onCreate()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
package net.buzzert.kordophonedroid
|
|
||||||
|
|
||||||
import android.Manifest
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.pm.PackageManager
|
|
||||||
import android.os.Bundle
|
|
||||||
import androidx.activity.ComponentActivity
|
|
||||||
import androidx.activity.compose.setContent
|
|
||||||
import androidx.core.app.ActivityCompat
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
|
||||||
import net.buzzert.kordophonedroid.ui.KordophoneApp
|
|
||||||
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
|
||||||
class MainActivity : ComponentActivity() {
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
|
|
||||||
// Ask for notifications
|
|
||||||
val hasPermission = ActivityCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS)
|
|
||||||
if (hasPermission != PackageManager.PERMISSION_GRANTED) {
|
|
||||||
ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.POST_NOTIFICATIONS), 1234)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start update monitor service
|
|
||||||
val intent = Intent(this, UpdateMonitorService::class.java)
|
|
||||||
startService(intent)
|
|
||||||
|
|
||||||
setContent {
|
|
||||||
KordophoneApp()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,161 +0,0 @@
|
|||||||
package net.buzzert.kordophonedroid
|
|
||||||
|
|
||||||
import android.Manifest
|
|
||||||
import android.R
|
|
||||||
import android.app.Notification
|
|
||||||
import android.app.NotificationChannel
|
|
||||||
import android.app.NotificationChannelGroup
|
|
||||||
import android.app.NotificationManager
|
|
||||||
import android.app.PendingIntent
|
|
||||||
import android.app.Service
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.pm.PackageManager
|
|
||||||
import android.graphics.Color
|
|
||||||
import android.os.IBinder
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.core.app.ActivityCompat
|
|
||||||
import androidx.core.app.NotificationCompat
|
|
||||||
import androidx.core.app.NotificationManagerCompat
|
|
||||||
import androidx.core.net.toUri
|
|
||||||
import androidx.navigation.NavDeepLinkBuilder
|
|
||||||
import androidx.navigation.NavDeepLinkRequest
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.SupervisorJob
|
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import net.buzzert.kordophone.backend.model.Message
|
|
||||||
import net.buzzert.kordophone.backend.server.ChatRepository
|
|
||||||
import net.buzzert.kordophonedroid.ui.Destination
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
const val PUSH_CHANNEL_ID = "net.buzzert.kordophone.persistentNotification"
|
|
||||||
const val NEW_MESSAGE_CHANNEL_ID = "net.buzzert.kordophone.newMessage"
|
|
||||||
|
|
||||||
const val UPDATER_LOG = "UpdateService"
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
|
||||||
class UpdateMonitorService: Service()
|
|
||||||
{
|
|
||||||
@Inject lateinit var chatRepository: ChatRepository
|
|
||||||
|
|
||||||
private var newMessageID: Int = 0
|
|
||||||
private var watchJob: Job? = null
|
|
||||||
private val job = SupervisorJob()
|
|
||||||
private val scope = CoroutineScope(Dispatchers.IO + job)
|
|
||||||
|
|
||||||
private fun createNotificationChannel(channelId: String, channelName: String) {
|
|
||||||
val chan = NotificationChannel(channelId, channelName, NotificationManager.IMPORTANCE_DEFAULT)
|
|
||||||
chan.lockscreenVisibility = Notification.VISIBILITY_PRIVATE
|
|
||||||
chan.lightColor = Color.BLUE
|
|
||||||
|
|
||||||
val service = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
||||||
service.createNotificationChannel(chan)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate() {
|
|
||||||
super.onCreate()
|
|
||||||
|
|
||||||
Log.v(UPDATER_LOG, "UpdateMonitor onCreate: Begin watching for updates.")
|
|
||||||
|
|
||||||
createNotificationChannel(NEW_MESSAGE_CHANNEL_ID, "New Messages")
|
|
||||||
|
|
||||||
// Connect to monitor
|
|
||||||
chatRepository.beginWatchingForUpdates(scope)
|
|
||||||
|
|
||||||
// Connect to new message flow for notifications
|
|
||||||
watchJob?.cancel()
|
|
||||||
watchJob = scope.launch {
|
|
||||||
chatRepository.newMessages.collectLatest(::onReceiveNewMessage)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
|
||||||
createNotificationChannel(PUSH_CHANNEL_ID, "Update Monitor Service")
|
|
||||||
|
|
||||||
val notificationIntent = Intent(this, MainActivity::class.java)
|
|
||||||
val pendingIntent = PendingIntent.getActivity(
|
|
||||||
this,
|
|
||||||
0, notificationIntent, PendingIntent.FLAG_IMMUTABLE
|
|
||||||
)
|
|
||||||
|
|
||||||
val notification: Notification = NotificationCompat.Builder(this, PUSH_CHANNEL_ID)
|
|
||||||
.setContentTitle("Kordophone Connected")
|
|
||||||
.setContentText("Kordophone is listening for new messages.")
|
|
||||||
.setSmallIcon(R.drawable.sym_action_chat)
|
|
||||||
.setContentIntent(pendingIntent)
|
|
||||||
.setShowWhen(false)
|
|
||||||
.setSilent(true)
|
|
||||||
.setOngoing(true)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
startForeground(5738, notification)
|
|
||||||
|
|
||||||
// Restart if we get killed
|
|
||||||
return START_STICKY
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onReceiveNewMessage(message: Message) {
|
|
||||||
val hasPermission = ActivityCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS)
|
|
||||||
if (hasPermission != PackageManager.PERMISSION_GRANTED) {
|
|
||||||
Log.e(UPDATER_LOG, "No permissions to post notifications.")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message.conversation.unreadCount == 0) {
|
|
||||||
// Not unread.
|
|
||||||
Log.v(UPDATER_LOG, "Ignoring read message.")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message.sender == null) {
|
|
||||||
// From me.
|
|
||||||
Log.v(UPDATER_LOG, "Ignoring message from me.")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message.conversation.isGroupChat) {
|
|
||||||
// For now, since these can be noisy and there's no UI for changing it, ignore group chats.
|
|
||||||
Log.v(UPDATER_LOG, "Ignoring group chat message.")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val guid = message.conversation.guid
|
|
||||||
val deepLinkIntent = Intent(
|
|
||||||
Intent.ACTION_VIEW,
|
|
||||||
"kordophone://messages/$guid".toUri(),
|
|
||||||
this,
|
|
||||||
MainActivity::class.java
|
|
||||||
)
|
|
||||||
|
|
||||||
val pendingIntent = PendingIntent.getActivity(this, 0, deepLinkIntent, PendingIntent.FLAG_IMMUTABLE)
|
|
||||||
|
|
||||||
val groupId = message.conversation.guid
|
|
||||||
val notification = NotificationCompat.Builder(this, NEW_MESSAGE_CHANNEL_ID)
|
|
||||||
.setContentTitle(message.sender)
|
|
||||||
.setContentText(message.text)
|
|
||||||
.setSmallIcon(R.drawable.stat_notify_chat)
|
|
||||||
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
|
||||||
.setContentIntent(pendingIntent)
|
|
||||||
.setGroup(groupId)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
val manager = NotificationManagerCompat.from(this)
|
|
||||||
manager.notify(newMessageID++, notification)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBind(intent: Intent?): IBinder? {
|
|
||||||
// no binding
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroy() {
|
|
||||||
super.onDestroy()
|
|
||||||
|
|
||||||
chatRepository.stopWatchingForUpdates()
|
|
||||||
job.cancel()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
package net.buzzert.kordophonedroid.data
|
|
||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
import androidx.lifecycle.ViewModelStore
|
|
||||||
import androidx.lifecycle.ViewModelStoreOwner
|
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
|
||||||
import net.buzzert.kordophone.backend.server.ChatRepository
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@HiltViewModel
|
|
||||||
class AppContainer @Inject constructor(
|
|
||||||
val repository: ChatRepository
|
|
||||||
) : ViewModel() {
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,110 +0,0 @@
|
|||||||
package net.buzzert.kordophonedroid.ui
|
|
||||||
|
|
||||||
import androidx.compose.material.AlertDialog
|
|
||||||
import androidx.compose.material.Button
|
|
||||||
import androidx.compose.material.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.compositionLocalOf
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|
||||||
import androidx.navigation.NavHost
|
|
||||||
import androidx.navigation.NavHostController
|
|
||||||
import androidx.navigation.compose.NavHost
|
|
||||||
import androidx.navigation.compose.composable
|
|
||||||
import androidx.navigation.compose.rememberNavController
|
|
||||||
import androidx.navigation.navDeepLink
|
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
|
||||||
import net.buzzert.kordophone.backend.server.ChatRepository
|
|
||||||
|
|
||||||
import net.buzzert.kordophonedroid.data.AppContainer
|
|
||||||
import net.buzzert.kordophonedroid.ui.attachments.AttachmentViewer
|
|
||||||
import net.buzzert.kordophonedroid.ui.theme.KordophoneTheme
|
|
||||||
import net.buzzert.kordophonedroid.ui.conversationlist.ConversationListScreen
|
|
||||||
import net.buzzert.kordophonedroid.ui.messagelist.MessageListScreen
|
|
||||||
import net.buzzert.kordophonedroid.ui.settings.SettingsScreen
|
|
||||||
|
|
||||||
sealed class Destination(val route: String) {
|
|
||||||
object ConversationList : Destination("conversations")
|
|
||||||
|
|
||||||
object Settings : Destination("settings")
|
|
||||||
|
|
||||||
object MessageList : Destination("messages/{id}") {
|
|
||||||
fun createRoute(data: String) = "messages/$data"
|
|
||||||
}
|
|
||||||
|
|
||||||
object AttachmentViewer : Destination("attachment/{guid}") {
|
|
||||||
fun createRoute(guid: String) = "attachment/$guid"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val LocalNavController = compositionLocalOf<NavHostController> { error("No nav host") }
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun ErrorDialog(title: String, body: String, onDismiss: () -> Unit) {
|
|
||||||
AlertDialog(
|
|
||||||
onDismissRequest = { onDismiss() },
|
|
||||||
title = { Text(title) },
|
|
||||||
text = { Text(body) },
|
|
||||||
confirmButton = {
|
|
||||||
Button(onClick = { onDismiss() }) {
|
|
||||||
Text("OK")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun KordophoneApp(
|
|
||||||
appContainer: AppContainer = hiltViewModel(),
|
|
||||||
) {
|
|
||||||
KordophoneTheme {
|
|
||||||
val navController = rememberNavController()
|
|
||||||
val errorVisible = remember { mutableStateOf<ChatRepository.Error?>(null) }
|
|
||||||
val error = appContainer.repository.errorEncounteredChannel.collectAsStateWithLifecycle(
|
|
||||||
initialValue = null
|
|
||||||
)
|
|
||||||
|
|
||||||
LaunchedEffect(key1 = error.value) {
|
|
||||||
errorVisible.value = error.value
|
|
||||||
}
|
|
||||||
|
|
||||||
CompositionLocalProvider(LocalNavController provides navController) {
|
|
||||||
NavHost(
|
|
||||||
navController = navController,
|
|
||||||
startDestination = Destination.ConversationList.route,
|
|
||||||
) {
|
|
||||||
composable(Destination.ConversationList.route) {
|
|
||||||
ConversationListScreen()
|
|
||||||
}
|
|
||||||
|
|
||||||
composable(
|
|
||||||
route = Destination.MessageList.route,
|
|
||||||
deepLinks = listOf(navDeepLink { uriPattern = "kordophone://messages/{id}" })
|
|
||||||
) {
|
|
||||||
val conversationID = it.arguments?.getString("id")!!
|
|
||||||
MessageListScreen(conversationGUID = conversationID)
|
|
||||||
}
|
|
||||||
|
|
||||||
composable(Destination.Settings.route) {
|
|
||||||
SettingsScreen()
|
|
||||||
}
|
|
||||||
|
|
||||||
composable(Destination.AttachmentViewer.route) {
|
|
||||||
val guid = it.arguments?.getString("guid")!!
|
|
||||||
AttachmentViewer(attachmentGuid = guid)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
errorVisible.value?.let {
|
|
||||||
ErrorDialog(title = it.title, body = it.description) {
|
|
||||||
errorVisible.value = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,116 +0,0 @@
|
|||||||
package net.buzzert.kordophonedroid.ui.attachments
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
import coil.Coil
|
|
||||||
import coil.ImageLoader
|
|
||||||
import coil.ImageLoaderFactory
|
|
||||||
import coil.annotation.ExperimentalCoilApi
|
|
||||||
import coil.decode.DataSource
|
|
||||||
import coil.decode.ImageSource
|
|
||||||
import coil.disk.DiskCache
|
|
||||||
import coil.fetch.FetchResult
|
|
||||||
import coil.fetch.Fetcher
|
|
||||||
import coil.fetch.SourceResult
|
|
||||||
import coil.request.Options
|
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
|
||||||
import net.buzzert.kordophone.backend.server.ChatRepository
|
|
||||||
|
|
||||||
const val AVM_LOG: String = "AttachmentImageLoader"
|
|
||||||
|
|
||||||
data class AttachmentFetchData(
|
|
||||||
val guid: String,
|
|
||||||
val preview: Boolean = false
|
|
||||||
)
|
|
||||||
|
|
||||||
class AttachmentImageLoader(
|
|
||||||
private val repository: ChatRepository,
|
|
||||||
@ApplicationContext val application: Context,
|
|
||||||
) : ViewModel(), ImageLoaderFactory, Fetcher.Factory<AttachmentFetchData>
|
|
||||||
{
|
|
||||||
init {
|
|
||||||
// Register Coil image loader
|
|
||||||
Coil.setImageLoader(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun newImageLoader(): ImageLoader {
|
|
||||||
val factory = this
|
|
||||||
return ImageLoader.Builder(application)
|
|
||||||
.components {
|
|
||||||
// Adds the FetcherFactory
|
|
||||||
add(factory)
|
|
||||||
}
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun create(
|
|
||||||
data: AttachmentFetchData,
|
|
||||||
options: Options,
|
|
||||||
imageLoader: ImageLoader
|
|
||||||
): Fetcher {
|
|
||||||
return AttachmentFetcher(repository, application, data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class AttachmentFetcher(
|
|
||||||
val repository: ChatRepository,
|
|
||||||
val context: Context,
|
|
||||||
val data: AttachmentFetchData
|
|
||||||
): Fetcher {
|
|
||||||
val cache = DiskCache.Builder()
|
|
||||||
.directory(context.cacheDir.resolve("attachments"))
|
|
||||||
.maxSizePercent(0.02)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
val cacheKey: String get() { return data.guid + if (data.preview) "_preview" else "" }
|
|
||||||
|
|
||||||
@OptIn(ExperimentalCoilApi::class)
|
|
||||||
override suspend fun fetch(): FetchResult {
|
|
||||||
// Try loading from cache
|
|
||||||
var snapshot = cache.openSnapshot(cacheKey)
|
|
||||||
if (snapshot != null) {
|
|
||||||
Log.d(AVM_LOG, "Found attachment ${data.guid} in disk cache")
|
|
||||||
return SourceResult(
|
|
||||||
source = snapshot.toImageSource(),
|
|
||||||
dataSource = DataSource.DISK,
|
|
||||||
mimeType = null,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Log.d(AVM_LOG, "Loading attachment ${data.guid} from network")
|
|
||||||
val source = repository.fetchAttachmentDataSource(data.guid, data.preview)
|
|
||||||
|
|
||||||
// Save to cache
|
|
||||||
val editor = cache.openEditor(cacheKey)
|
|
||||||
if (editor != null) {
|
|
||||||
Log.d(AVM_LOG, "Writing attachment ${data.guid} to disk cache")
|
|
||||||
|
|
||||||
cache.fileSystem.write(editor.data) {
|
|
||||||
source.readAll(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
snapshot = editor.commitAndOpenSnapshot()
|
|
||||||
if (snapshot != null) {
|
|
||||||
return SourceResult(
|
|
||||||
source = snapshot.toImageSource(),
|
|
||||||
dataSource = DataSource.NETWORK,
|
|
||||||
mimeType = null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// We'll go down this path if for some reason we couldn't save to cache.
|
|
||||||
return SourceResult(
|
|
||||||
source = ImageSource(source, context),
|
|
||||||
dataSource = DataSource.NETWORK,
|
|
||||||
mimeType = null,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalCoilApi::class)
|
|
||||||
private fun DiskCache.Snapshot.toImageSource(): ImageSource {
|
|
||||||
val fileSystem = cache.fileSystem
|
|
||||||
return ImageSource(data, fileSystem, cacheKey, this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
package net.buzzert.kordophonedroid.ui.attachments
|
|
||||||
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.material.CircularProgressIndicator
|
|
||||||
import androidx.compose.material.Scaffold
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import coil.compose.SubcomposeAsyncImage
|
|
||||||
import coil.request.ImageRequest
|
|
||||||
import net.buzzert.kordophonedroid.ui.LocalNavController
|
|
||||||
import net.buzzert.kordophonedroid.ui.theme.KordophoneTopAppBar
|
|
||||||
import net.engawapg.lib.zoomable.rememberZoomState
|
|
||||||
import net.engawapg.lib.zoomable.zoomable
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun AttachmentViewer(attachmentGuid: String) {
|
|
||||||
var topBarVisible = remember { mutableStateOf(true) }
|
|
||||||
val navController = LocalNavController.current
|
|
||||||
Scaffold(topBar = {
|
|
||||||
KordophoneTopAppBar(
|
|
||||||
title = "Attachment",
|
|
||||||
backAction = { navController.popBackStack() },
|
|
||||||
visible = topBarVisible.value
|
|
||||||
)
|
|
||||||
}) { padding ->
|
|
||||||
val zoomState = rememberZoomState()
|
|
||||||
val interactionSource = remember { MutableInteractionSource() }
|
|
||||||
val data = AttachmentFetchData(attachmentGuid, preview = false)
|
|
||||||
Column(modifier = Modifier.padding(padding)) {
|
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
|
||||||
|
|
||||||
SubcomposeAsyncImage(
|
|
||||||
model = ImageRequest.Builder(LocalContext.current)
|
|
||||||
.data(data)
|
|
||||||
.crossfade(true)
|
|
||||||
.build(),
|
|
||||||
contentDescription = "",
|
|
||||||
loading = {
|
|
||||||
Box {
|
|
||||||
CircularProgressIndicator(
|
|
||||||
modifier = Modifier.align(Alignment.Center)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
modifier = Modifier
|
|
||||||
.zoomable(zoomState)
|
|
||||||
.fillMaxWidth()
|
|
||||||
.align(Alignment.CenterHorizontally)
|
|
||||||
.clickable(
|
|
||||||
interactionSource = interactionSource,
|
|
||||||
indication = null
|
|
||||||
) {
|
|
||||||
topBarVisible.value = !topBarVisible.value
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,248 +0,0 @@
|
|||||||
package net.buzzert.kordophonedroid.ui.conversationlist
|
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.layout.width
|
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
|
||||||
import androidx.compose.foundation.lazy.items
|
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
|
||||||
import androidx.compose.material.Divider
|
|
||||||
import androidx.compose.material.ExperimentalMaterialApi
|
|
||||||
import androidx.compose.material.Icon
|
|
||||||
import androidx.compose.material.IconButton
|
|
||||||
import androidx.compose.material.MaterialTheme
|
|
||||||
import androidx.compose.material.Scaffold
|
|
||||||
import androidx.compose.material.Text
|
|
||||||
import androidx.compose.material.TopAppBar
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.rounded.Settings
|
|
||||||
import androidx.compose.material.pullrefresh.PullRefreshIndicator
|
|
||||||
import androidx.compose.material.pullrefresh.pullRefresh
|
|
||||||
import androidx.compose.material.pullrefresh.rememberPullRefreshState
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.text.TextStyle
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
|
||||||
import androidx.compose.ui.unit.Dp
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.compose.ui.unit.sp
|
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import net.buzzert.kordophone.backend.model.Conversation
|
|
||||||
import net.buzzert.kordophonedroid.R
|
|
||||||
import net.buzzert.kordophonedroid.ui.Destination
|
|
||||||
import net.buzzert.kordophonedroid.ui.LocalNavController
|
|
||||||
import java.time.LocalDate
|
|
||||||
import java.time.LocalDateTime
|
|
||||||
import java.time.ZoneId
|
|
||||||
import java.time.format.DateTimeFormatter
|
|
||||||
import java.util.Date
|
|
||||||
|
|
||||||
fun formatDateTime(dateTime: LocalDateTime): String {
|
|
||||||
val formatter: DateTimeFormatter = if (LocalDate.now().isEqual(dateTime.toLocalDate())) {
|
|
||||||
DateTimeFormatter.ofPattern("HH:mm") // show just the time
|
|
||||||
} else {
|
|
||||||
DateTimeFormatter.ofPattern("M/d/yy") // show day/month/year
|
|
||||||
}
|
|
||||||
|
|
||||||
return dateTime.format(formatter)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun ConversationListScreen(
|
|
||||||
viewModel: ConversationListViewModel = hiltViewModel(),
|
|
||||||
) {
|
|
||||||
val conversations by viewModel.conversations.collectAsStateWithLifecycle(initialValue = emptyList())
|
|
||||||
val encounteredError by viewModel.encounteredConnectionError
|
|
||||||
|
|
||||||
ConversationListView(
|
|
||||||
conversations = conversations,
|
|
||||||
isConfigured = viewModel.isServerConfigured,
|
|
||||||
encounteredError = encounteredError,
|
|
||||||
onRefresh = suspend { viewModel.refresh() }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
@OptIn(ExperimentalMaterialApi::class)
|
|
||||||
fun ConversationListView(
|
|
||||||
conversations: List<Conversation>,
|
|
||||||
isConfigured: Boolean = true,
|
|
||||||
encounteredError: Boolean = false,
|
|
||||||
onRefresh: suspend () -> Unit,
|
|
||||||
) {
|
|
||||||
val listState = rememberLazyListState()
|
|
||||||
|
|
||||||
val refreshScope = rememberCoroutineScope()
|
|
||||||
var refreshing by remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
fun refresh() = refreshScope.launch {
|
|
||||||
refreshing = true
|
|
||||||
onRefresh()
|
|
||||||
refreshing = false
|
|
||||||
}
|
|
||||||
|
|
||||||
val showErrorScreen = conversations.isEmpty() && encounteredError
|
|
||||||
|
|
||||||
val refreshState = rememberPullRefreshState(refreshing = refreshing, onRefresh = ::refresh)
|
|
||||||
|
|
||||||
val navController = LocalNavController.current
|
|
||||||
val onSettingsInvoked = { navController.navigate(Destination.Settings.route) }
|
|
||||||
Scaffold(
|
|
||||||
topBar = {
|
|
||||||
TopAppBar(title = { Text("Conversations") }, actions = {
|
|
||||||
IconButton(onClick = onSettingsInvoked) {
|
|
||||||
Icon(Icons.Rounded.Settings, contentDescription = "Settings")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
if (showErrorScreen) {
|
|
||||||
NoContentView(
|
|
||||||
icon = R.drawable.error,
|
|
||||||
text = "Connection error",
|
|
||||||
onSettings = onSettingsInvoked
|
|
||||||
)
|
|
||||||
} else if (!isConfigured) {
|
|
||||||
NoContentView(
|
|
||||||
icon = R.drawable.storage,
|
|
||||||
text = "Server not configured",
|
|
||||||
onSettings = onSettingsInvoked
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
Box(Modifier.pullRefresh(refreshState)) {
|
|
||||||
LazyColumn(
|
|
||||||
state = listState,
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(it)
|
|
||||||
.fillMaxSize()
|
|
||||||
) {
|
|
||||||
items(conversations) { conversation ->
|
|
||||||
val clickHandler = {
|
|
||||||
val route = Destination.MessageList.createRoute(conversation.guid)
|
|
||||||
navController.navigate(route)
|
|
||||||
}
|
|
||||||
|
|
||||||
ConversationListItem(
|
|
||||||
name = conversation.formattedDisplayName(),
|
|
||||||
id = conversation.guid,
|
|
||||||
isUnread = conversation.unreadCount > 0,
|
|
||||||
lastMessagePreview = conversation.lastMessagePreview ?: "",
|
|
||||||
date = conversation.date,
|
|
||||||
onClick = clickHandler
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
PullRefreshIndicator(
|
|
||||||
refreshing = refreshing,
|
|
||||||
state = refreshState,
|
|
||||||
modifier = Modifier.align(Alignment.TopCenter),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun ConversationListItem(
|
|
||||||
name: String,
|
|
||||||
id: String,
|
|
||||||
isUnread: Boolean,
|
|
||||||
lastMessagePreview: String,
|
|
||||||
date: Date,
|
|
||||||
onClick: () -> Unit
|
|
||||||
) {
|
|
||||||
val unreadSize = 12.dp
|
|
||||||
val horizontalPadding = 8.dp
|
|
||||||
val verticalPadding = 14.dp
|
|
||||||
|
|
||||||
Row(
|
|
||||||
Modifier
|
|
||||||
.clickable(onClick = onClick)
|
|
||||||
) {
|
|
||||||
Spacer(Modifier.width(horizontalPadding))
|
|
||||||
|
|
||||||
// Unread icon
|
|
||||||
if (isUnread) {
|
|
||||||
UnreadIndicator(
|
|
||||||
size = unreadSize,
|
|
||||||
modifier = Modifier.align(Alignment.CenterVertically)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
Spacer(modifier = Modifier.size(unreadSize))
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(Modifier.width(horizontalPadding))
|
|
||||||
|
|
||||||
Column {
|
|
||||||
Spacer(Modifier.height(verticalPadding))
|
|
||||||
Row(
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
name,
|
|
||||||
style = TextStyle(fontSize = 18.sp, fontWeight = FontWeight.Bold),
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
maxLines = 1,
|
|
||||||
modifier = Modifier.weight(1f, fill = true)
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier)
|
|
||||||
|
|
||||||
Text(
|
|
||||||
formatDateTime(
|
|
||||||
date.toInstant()
|
|
||||||
.atZone(ZoneId.systemDefault())
|
|
||||||
.toLocalDateTime()
|
|
||||||
),
|
|
||||||
color = MaterialTheme.colors.onBackground.copy(alpha = 0.4f),
|
|
||||||
maxLines = 1,
|
|
||||||
modifier = Modifier
|
|
||||||
.align(Alignment.CenterVertically)
|
|
||||||
,
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(Modifier.width(horizontalPadding))
|
|
||||||
}
|
|
||||||
|
|
||||||
Text(lastMessagePreview, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
|
||||||
|
|
||||||
Spacer(Modifier.height(verticalPadding))
|
|
||||||
Divider()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun UnreadIndicator(size: Dp, modifier: Modifier = Modifier) {
|
|
||||||
Box(
|
|
||||||
modifier = modifier
|
|
||||||
.size(size)
|
|
||||||
.background(
|
|
||||||
color = MaterialTheme.colors.primary,
|
|
||||||
shape = CircleShape
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
package net.buzzert.kordophonedroid.ui.conversationlist
|
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.compose.runtime.State
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
|
||||||
import kotlinx.coroutines.flow.map
|
|
||||||
import kotlinx.coroutines.flow.shareIn
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import net.buzzert.kordophone.backend.model.Conversation
|
|
||||||
import net.buzzert.kordophone.backend.server.APIClientFactory
|
|
||||||
import net.buzzert.kordophone.backend.server.ChatRepository
|
|
||||||
import net.buzzert.kordophonedroid.ui.shared.ServerConfigRepository
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
const val CL_VM_LOG: String = "ConversationListViewModel"
|
|
||||||
|
|
||||||
@HiltViewModel
|
|
||||||
class ConversationListViewModel @Inject constructor(
|
|
||||||
private val chatRepository: ChatRepository,
|
|
||||||
private val serverConfigRepository: ServerConfigRepository,
|
|
||||||
) : ViewModel() {
|
|
||||||
val conversations: Flow<List<Conversation>>
|
|
||||||
get() = chatRepository.conversationChanges
|
|
||||||
.shareIn(viewModelScope, started = SharingStarted.WhileSubscribed())
|
|
||||||
.map {
|
|
||||||
it.sortedBy { it.date }
|
|
||||||
.reversed()
|
|
||||||
}
|
|
||||||
|
|
||||||
val isServerConfigured: Boolean
|
|
||||||
get() = chatRepository.isConfigured
|
|
||||||
|
|
||||||
val encounteredConnectionError: State<Boolean>
|
|
||||||
get() = _encounteredConnectionError
|
|
||||||
|
|
||||||
private val _encounteredConnectionError = mutableStateOf(false)
|
|
||||||
|
|
||||||
init {
|
|
||||||
// Watch for config changes
|
|
||||||
viewModelScope.launch {
|
|
||||||
serverConfigRepository.serverConfig.collect { config ->
|
|
||||||
Log.d(CL_VM_LOG, "Got settings change.")
|
|
||||||
|
|
||||||
// Make new APIClient
|
|
||||||
val baseURL = config.serverName
|
|
||||||
val authentication = config.authentication?.toBackendAuthentication()
|
|
||||||
val apiClient = APIClientFactory.createClient(baseURL, authentication)
|
|
||||||
chatRepository.updateAPIClient(apiClient)
|
|
||||||
|
|
||||||
// Perform db synchronization
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
chatRepository.synchronize()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
viewModelScope.launch {
|
|
||||||
chatRepository.errorEncounteredChannel.collect {
|
|
||||||
_encounteredConnectionError.value = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun refresh() {
|
|
||||||
chatRepository.synchronize()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
package net.buzzert.kordophonedroid.ui.conversationlist
|
|
||||||
|
|
||||||
import androidx.annotation.DrawableRes
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.width
|
|
||||||
import androidx.compose.material.Button
|
|
||||||
import androidx.compose.material.Icon
|
|
||||||
import androidx.compose.material.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.draw.alpha
|
|
||||||
import androidx.compose.ui.res.painterResource
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.compose.ui.unit.em
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun NoContentView(
|
|
||||||
@DrawableRes icon: Int,
|
|
||||||
text: String,
|
|
||||||
onSettings: () -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.fillMaxHeight(),
|
|
||||||
verticalArrangement = Arrangement.Center,
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
painter = painterResource(icon),
|
|
||||||
"server icon",
|
|
||||||
modifier = Modifier
|
|
||||||
.height(150.dp)
|
|
||||||
.width(150.dp)
|
|
||||||
.alpha(0.5F)
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(Modifier)
|
|
||||||
|
|
||||||
Text(
|
|
||||||
text = text,
|
|
||||||
fontSize = 5.0.em,
|
|
||||||
modifier = Modifier
|
|
||||||
.alpha(0.5F)
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
|
||||||
|
|
||||||
Button(onClick = onSettings) {
|
|
||||||
Text("Settings")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,166 +0,0 @@
|
|||||||
package net.buzzert.kordophonedroid.ui.messagelist
|
|
||||||
|
|
||||||
import androidx.compose.foundation.Image
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.aspectRatio
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.imePadding
|
|
||||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.width
|
|
||||||
import androidx.compose.foundation.lazy.LazyRow
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.foundation.text.BasicTextField
|
|
||||||
import androidx.compose.material.Button
|
|
||||||
import androidx.compose.material3.ButtonDefaults
|
|
||||||
import androidx.compose.material.Divider
|
|
||||||
import androidx.compose.material.Icon
|
|
||||||
import androidx.compose.material.IconButton
|
|
||||||
import androidx.compose.material.MaterialTheme
|
|
||||||
import androidx.compose.material.Surface
|
|
||||||
import androidx.compose.material.Text
|
|
||||||
import androidx.compose.material3.ElevatedButton
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.draw.clip
|
|
||||||
import androidx.compose.ui.draw.shadow
|
|
||||||
import androidx.compose.ui.graphics.SolidColor
|
|
||||||
import androidx.compose.ui.graphics.painter.Painter
|
|
||||||
import androidx.compose.ui.layout.ContentScale
|
|
||||||
import androidx.compose.ui.res.painterResource
|
|
||||||
import androidx.compose.ui.text.input.TextFieldValue
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import net.buzzert.kordophonedroid.R
|
|
||||||
|
|
||||||
data class AttachmentRowItem(
|
|
||||||
val painter: Painter,
|
|
||||||
val id: String,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun AttachmentRow(
|
|
||||||
attachmentItems: List<AttachmentRowItem>,
|
|
||||||
onClear: () -> Unit,
|
|
||||||
) {
|
|
||||||
Divider()
|
|
||||||
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.height(120.dp)
|
|
||||||
.fillMaxWidth()
|
|
||||||
.background(MaterialTheme.colors.onSurface.copy(0.08f))
|
|
||||||
.padding(8.dp)
|
|
||||||
) {
|
|
||||||
LazyRow {
|
|
||||||
attachmentItems.forEach { attachmentItem ->
|
|
||||||
item {
|
|
||||||
Image(
|
|
||||||
painter = attachmentItem.painter,
|
|
||||||
contentDescription = "attachment",
|
|
||||||
contentScale = ContentScale.Crop,
|
|
||||||
modifier = Modifier
|
|
||||||
.aspectRatio(1.0f)
|
|
||||||
.clip(RoundedCornerShape(4.dp))
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(Modifier.width(4.dp))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(Modifier.weight(1f))
|
|
||||||
|
|
||||||
ElevatedButton(
|
|
||||||
onClick = onClear,
|
|
||||||
colors = ButtonDefaults.elevatedButtonColors(
|
|
||||||
containerColor = MaterialTheme.colors.background
|
|
||||||
),
|
|
||||||
modifier = Modifier.align(Alignment.CenterVertically)
|
|
||||||
) {
|
|
||||||
Text("Remove")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun MessageEntry(
|
|
||||||
textFieldValue: TextFieldValue,
|
|
||||||
attachmentItems: List<AttachmentRowItem> = listOf(),
|
|
||||||
|
|
||||||
onAddAttachment: () -> Unit = {},
|
|
||||||
onClearAttachments: () -> Unit = {},
|
|
||||||
onTextChanged: (TextFieldValue) -> Unit,
|
|
||||||
onSend: () -> Unit,
|
|
||||||
) {
|
|
||||||
Column {
|
|
||||||
if (attachmentItems.isNotEmpty()) {
|
|
||||||
AttachmentRow(attachmentItems, onClear = onClearAttachments)
|
|
||||||
}
|
|
||||||
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.background(MaterialTheme.colors.onSurface.copy(alpha = 0.18f))
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(vertical = 8.dp, horizontal = 4.dp)
|
|
||||||
.imePadding()
|
|
||||||
.navigationBarsPadding()
|
|
||||||
) {
|
|
||||||
IconButton(
|
|
||||||
onClick = onAddAttachment,
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
painter = painterResource(id = R.drawable.attach_file),
|
|
||||||
contentDescription = "Attach File"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(Modifier.width(8.dp))
|
|
||||||
|
|
||||||
Surface(
|
|
||||||
shape = MaterialTheme.shapes.medium,
|
|
||||||
modifier = Modifier
|
|
||||||
.weight(1f)
|
|
||||||
.align(Alignment.CenterVertically)
|
|
||||||
.shadow(3.dp)
|
|
||||||
) {
|
|
||||||
BasicTextField(
|
|
||||||
value = textFieldValue,
|
|
||||||
onValueChange = { onTextChanged(it) },
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = 8.dp, vertical = 8.dp),
|
|
||||||
cursorBrush = SolidColor(MaterialTheme.colors.onBackground),
|
|
||||||
textStyle = MaterialTheme.typography.body1.copy(MaterialTheme.colors.onBackground),
|
|
||||||
decorationBox = { textContent ->
|
|
||||||
if (textFieldValue.text.isEmpty()) {
|
|
||||||
Text(
|
|
||||||
text = "Message",
|
|
||||||
style = MaterialTheme.typography.body1.copy(
|
|
||||||
color = MaterialTheme.colors.onSurface.copy(alpha = 0.4f)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
textContent()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(Modifier.width(8.dp))
|
|
||||||
|
|
||||||
Button(
|
|
||||||
onClick = onSend,
|
|
||||||
enabled = (attachmentItems.isNotEmpty() || textFieldValue.text.isNotEmpty())
|
|
||||||
) {
|
|
||||||
Text(text = "Send")
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(Modifier.width(8.dp))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,433 +0,0 @@
|
|||||||
package net.buzzert.kordophonedroid.ui.messagelist
|
|
||||||
|
|
||||||
import android.net.Uri
|
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
|
||||||
import androidx.compose.foundation.lazy.LazyListState
|
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.foundation.text.ClickableText
|
|
||||||
import androidx.compose.material.CircularProgressIndicator
|
|
||||||
import androidx.compose.material.MaterialTheme
|
|
||||||
import androidx.compose.material.Scaffold
|
|
||||||
import androidx.compose.material.Surface
|
|
||||||
import androidx.compose.material.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.draw.alpha
|
|
||||||
import androidx.compose.ui.draw.clip
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.platform.LocalUriHandler
|
|
||||||
import androidx.compose.ui.text.TextStyle
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.text.input.TextFieldValue
|
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.core.app.NotificationManagerCompat
|
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|
||||||
import coil.compose.SubcomposeAsyncImage
|
|
||||||
import coil.compose.rememberAsyncImagePainter
|
|
||||||
import coil.request.ImageRequest
|
|
||||||
import net.buzzert.kordophone.backend.model.GUID
|
|
||||||
import net.buzzert.kordophonedroid.ui.Destination
|
|
||||||
import net.buzzert.kordophonedroid.ui.LocalNavController
|
|
||||||
import net.buzzert.kordophonedroid.ui.attachments.AttachmentFetchData
|
|
||||||
import net.buzzert.kordophonedroid.ui.shared.LINK_ANNOTATION_TAG
|
|
||||||
import net.buzzert.kordophonedroid.ui.shared.linkify
|
|
||||||
import net.buzzert.kordophonedroid.ui.theme.KordophoneTopAppBar
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.time.Duration
|
|
||||||
import java.util.Date
|
|
||||||
|
|
||||||
private val IncomingChatBubbleShape = RoundedCornerShape(4.dp, 20.dp, 20.dp, 20.dp)
|
|
||||||
private val OutgoingChatBubbleShape = RoundedCornerShape(20.dp, 4.dp, 20.dp, 20.dp)
|
|
||||||
|
|
||||||
data class MessageMetadata(
|
|
||||||
val fromAddress: String,
|
|
||||||
val fromMe: Boolean,
|
|
||||||
val date: Date,
|
|
||||||
val delivered: Boolean = true,
|
|
||||||
)
|
|
||||||
|
|
||||||
interface MessageMetadataProvider {
|
|
||||||
val metadata: MessageMetadata
|
|
||||||
}
|
|
||||||
|
|
||||||
sealed class MessageListItem: MessageMetadataProvider {
|
|
||||||
data class TextMessage(val text: String, override val metadata: MessageMetadata): MessageListItem()
|
|
||||||
data class ImageAttachmentMessage(val guid: String, override val metadata: MessageMetadata): MessageListItem()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun MessageListScreen(
|
|
||||||
conversationGUID: GUID,
|
|
||||||
viewModel: MessageListViewModel = hiltViewModel(),
|
|
||||||
) {
|
|
||||||
viewModel.conversationGUID = conversationGUID
|
|
||||||
|
|
||||||
// Synchronize on launch
|
|
||||||
val context = LocalContext.current
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
// Clear notifications for this conversation
|
|
||||||
with(NotificationManagerCompat.from(context)) {
|
|
||||||
// Not sure how to cancel individual notifications, or groups yet...
|
|
||||||
cancelAll()
|
|
||||||
}
|
|
||||||
|
|
||||||
viewModel.markAsRead()
|
|
||||||
viewModel.synchronize()
|
|
||||||
}
|
|
||||||
|
|
||||||
val messages by viewModel.messages.collectAsStateWithLifecycle(initialValue = listOf())
|
|
||||||
|
|
||||||
val messageItems = mutableListOf<MessageListItem>()
|
|
||||||
for (message in messages) {
|
|
||||||
val metadata = MessageMetadata(
|
|
||||||
fromMe = message.sender == null,
|
|
||||||
date = message.date,
|
|
||||||
fromAddress = message.sender ?: "<me>",
|
|
||||||
delivered = !viewModel.isPendingMessage(message)
|
|
||||||
)
|
|
||||||
|
|
||||||
// Collect attachments
|
|
||||||
message.attachmentGUIDs?.let { guids ->
|
|
||||||
guids.forEach { guid ->
|
|
||||||
val item = MessageListItem.ImageAttachmentMessage(
|
|
||||||
guid = guid,
|
|
||||||
metadata = metadata
|
|
||||||
)
|
|
||||||
|
|
||||||
messageItems.add(item)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val displayText = message.displayText.trim()
|
|
||||||
if (displayText.isNotEmpty()) {
|
|
||||||
val textMessage = MessageListItem.TextMessage(text = displayText, metadata = metadata)
|
|
||||||
messageItems.add(textMessage)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var attachmentUris by remember { mutableStateOf<Set<Uri>>(mutableSetOf()) }
|
|
||||||
val imagePicker = rememberLauncherForActivityResult(contract = ActivityResultContracts.GetContent()) { uri: Uri? ->
|
|
||||||
uri?.let {
|
|
||||||
attachmentUris = attachmentUris.plus(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val navController = LocalNavController.current
|
|
||||||
Scaffold(
|
|
||||||
topBar = {
|
|
||||||
KordophoneTopAppBar(title = viewModel.title, backAction = { navController.popBackStack() })
|
|
||||||
}) { padding ->
|
|
||||||
MessageTranscript(
|
|
||||||
messages = messageItems,
|
|
||||||
paddingValues = padding,
|
|
||||||
showSenders = viewModel.isGroupChat,
|
|
||||||
attachmentUris = attachmentUris,
|
|
||||||
onAddAttachment = {
|
|
||||||
imagePicker.launch("image/*")
|
|
||||||
},
|
|
||||||
onClearAttachments = {
|
|
||||||
attachmentUris = setOf()
|
|
||||||
},
|
|
||||||
onSendMessage = { text ->
|
|
||||||
viewModel.enqueueOutgoingMessage(
|
|
||||||
text = text,
|
|
||||||
attachmentUris = attachmentUris,
|
|
||||||
context = context
|
|
||||||
)
|
|
||||||
|
|
||||||
// Clear pending attachments
|
|
||||||
attachmentUris = setOf()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun MessageTranscript(
|
|
||||||
messages: List<MessageListItem>,
|
|
||||||
paddingValues: PaddingValues,
|
|
||||||
showSenders: Boolean,
|
|
||||||
attachmentUris: Set<Uri>,
|
|
||||||
onAddAttachment: () -> Unit,
|
|
||||||
onClearAttachments: () -> Unit,
|
|
||||||
onSendMessage: (text: String) -> Unit,
|
|
||||||
) {
|
|
||||||
val scrollState = rememberLazyListState()
|
|
||||||
var textState by rememberSaveable(stateSaver = TextFieldValue.Saver) {
|
|
||||||
mutableStateOf(TextFieldValue())
|
|
||||||
}
|
|
||||||
|
|
||||||
val attachmentRowItems = attachmentUris.map {
|
|
||||||
AttachmentRowItem(
|
|
||||||
painter = rememberAsyncImagePainter(model = it),
|
|
||||||
id = "attachmentID"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Column(
|
|
||||||
Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.padding(paddingValues)) {
|
|
||||||
|
|
||||||
Messages(
|
|
||||||
messages = messages,
|
|
||||||
modifier = Modifier.weight(1f),
|
|
||||||
showSenders = showSenders,
|
|
||||||
scrollState = scrollState
|
|
||||||
)
|
|
||||||
|
|
||||||
MessageEntry(
|
|
||||||
onTextChanged = { textState = it },
|
|
||||||
textFieldValue = textState,
|
|
||||||
attachmentItems = attachmentRowItems,
|
|
||||||
onAddAttachment = onAddAttachment,
|
|
||||||
onClearAttachments = onClearAttachments,
|
|
||||||
onSend = {
|
|
||||||
onSendMessage(textState.text)
|
|
||||||
|
|
||||||
// Clear text state
|
|
||||||
textState = TextFieldValue()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun Messages(
|
|
||||||
messages: List<MessageListItem>,
|
|
||||||
showSenders: Boolean,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
scrollState: LazyListState
|
|
||||||
) {
|
|
||||||
Box(modifier = modifier) {
|
|
||||||
LazyColumn(
|
|
||||||
reverseLayout = true,
|
|
||||||
state = scrollState,
|
|
||||||
contentPadding = PaddingValues(vertical = 8.dp),
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.padding(horizontal = 16.dp)
|
|
||||||
) {
|
|
||||||
val dateFormatter = SimpleDateFormat.getDateTimeInstance()
|
|
||||||
|
|
||||||
for (index in messages.indices) {
|
|
||||||
val content = messages[index]
|
|
||||||
|
|
||||||
var previousMessage: MessageListItem? = null
|
|
||||||
if ((index + 1) < messages.count()) {
|
|
||||||
previousMessage = messages[index + 1]
|
|
||||||
}
|
|
||||||
|
|
||||||
val duration: Duration? = if (previousMessage != null) Duration.between(
|
|
||||||
previousMessage.metadata.date.toInstant(),
|
|
||||||
content.metadata.date.toInstant()
|
|
||||||
) else null
|
|
||||||
|
|
||||||
val leapMessage = (
|
|
||||||
duration == null || (
|
|
||||||
duration.toMinutes() > 30
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
val repeatMessage = !leapMessage && (
|
|
||||||
previousMessage == null || (
|
|
||||||
(previousMessage.metadata.fromAddress == content.metadata.fromAddress)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
// Remember: This is upside down.
|
|
||||||
item {
|
|
||||||
when (content) {
|
|
||||||
is MessageListItem.TextMessage -> {
|
|
||||||
MessageBubble(
|
|
||||||
text = content.text,
|
|
||||||
mine = content.metadata.fromMe,
|
|
||||||
modifier = Modifier
|
|
||||||
.alpha(if (!content.metadata.delivered) 0.5F else 1.0f)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
is MessageListItem.ImageAttachmentMessage -> {
|
|
||||||
ImageBubble(guid = content.guid, mine = content.metadata.fromMe)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Sender
|
|
||||||
if (!content.metadata.fromMe && showSenders && !repeatMessage) {
|
|
||||||
Text(
|
|
||||||
text = content.metadata.fromAddress,
|
|
||||||
style = MaterialTheme.typography.subtitle2.copy(
|
|
||||||
color = MaterialTheme.colors.onSurface.copy(alpha = 0.7f)
|
|
||||||
),
|
|
||||||
modifier = Modifier.padding(vertical = 8.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Greater than 30 minutes: show date:
|
|
||||||
if (duration != null) {
|
|
||||||
if (duration.toMinutes() > 30) {
|
|
||||||
val formattedDate = dateFormatter.format(content.metadata.date)
|
|
||||||
Text(
|
|
||||||
text = formattedDate,
|
|
||||||
textAlign = TextAlign.Center,
|
|
||||||
style = MaterialTheme.typography.caption.copy(
|
|
||||||
color = MaterialTheme.colors.onSurface.copy(alpha = 0.4f)
|
|
||||||
),
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(18.dp),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Greater than five minutes: add a bit of space.
|
|
||||||
else if (duration.toMinutes() > 5) {
|
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun BubbleScaffold(
|
|
||||||
mine: Boolean,
|
|
||||||
modifier: Modifier,
|
|
||||||
content: @Composable () -> Unit
|
|
||||||
) {
|
|
||||||
Column() {
|
|
||||||
Row(modifier = modifier.fillMaxWidth()) {
|
|
||||||
if (mine) {
|
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
|
||||||
}
|
|
||||||
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(0.8f),
|
|
||||||
horizontalArrangement = if (mine) Arrangement.End else Arrangement.Start,
|
|
||||||
) {
|
|
||||||
content()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!mine) { Spacer(modifier = Modifier.weight(1f)) }
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun MessageBubble(
|
|
||||||
text: String,
|
|
||||||
mine: Boolean,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
|
||||||
val backgroundBubbleColor = if (mine) MaterialTheme.colors.primary else MaterialTheme.colors.secondary
|
|
||||||
|
|
||||||
// Linkify text
|
|
||||||
val annotatedString = text.linkify()
|
|
||||||
val urlHandler = LocalUriHandler.current
|
|
||||||
|
|
||||||
BubbleScaffold(mine = mine, modifier = modifier) {
|
|
||||||
Surface(
|
|
||||||
color = backgroundBubbleColor,
|
|
||||||
shape = if (mine) OutgoingChatBubbleShape else IncomingChatBubbleShape,
|
|
||||||
) {
|
|
||||||
ClickableText(
|
|
||||||
text = annotatedString,
|
|
||||||
style = MaterialTheme.typography.body2,
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(12.dp),
|
|
||||||
onClick = { index ->
|
|
||||||
annotatedString
|
|
||||||
.getStringAnnotations(LINK_ANNOTATION_TAG, index, index)
|
|
||||||
.firstOrNull()?.let {
|
|
||||||
urlHandler.openUri(it.item)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun ImageBubble(
|
|
||||||
guid: String,
|
|
||||||
mine: Boolean,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
|
||||||
val shape: RoundedCornerShape = if (mine) OutgoingChatBubbleShape else IncomingChatBubbleShape
|
|
||||||
val attachmentFetchData = AttachmentFetchData(guid, preview = true)
|
|
||||||
val navController = LocalNavController.current
|
|
||||||
|
|
||||||
BubbleScaffold(mine = mine, modifier = modifier) {
|
|
||||||
SubcomposeAsyncImage(
|
|
||||||
model = ImageRequest.Builder(LocalContext.current)
|
|
||||||
.data(attachmentFetchData)
|
|
||||||
.crossfade(true)
|
|
||||||
.build(),
|
|
||||||
loading = {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.background(Color.LightGray)
|
|
||||||
.size(width = 220.dp, height = 200.dp)
|
|
||||||
) {
|
|
||||||
CircularProgressIndicator(
|
|
||||||
modifier = Modifier.align(Alignment.Center)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
error = {
|
|
||||||
val error = it.result.throwable.message
|
|
||||||
Surface(
|
|
||||||
color = Color.Red
|
|
||||||
) {
|
|
||||||
Column(modifier = Modifier.padding(20.dp)) {
|
|
||||||
Text(
|
|
||||||
text = "Error loading attachment",
|
|
||||||
style = TextStyle.Default.copy(
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
textAlign = TextAlign.Center,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
Text("$error")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
contentDescription = "Image attachment",
|
|
||||||
modifier = Modifier
|
|
||||||
.clip(shape)
|
|
||||||
.clickable {
|
|
||||||
navController.navigate(Destination.AttachmentViewer.createRoute(guid))
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
package net.buzzert.kordophonedroid.ui.messagelist
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.net.Uri
|
|
||||||
import android.webkit.MimeTypeMap
|
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
|
||||||
import kotlinx.coroutines.flow.combine
|
|
||||||
import kotlinx.coroutines.flow.map
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import net.buzzert.kordophone.backend.model.Conversation
|
|
||||||
import net.buzzert.kordophone.backend.model.GUID
|
|
||||||
import net.buzzert.kordophone.backend.model.Message
|
|
||||||
import net.buzzert.kordophone.backend.model.OutgoingMessage
|
|
||||||
import net.buzzert.kordophone.backend.model.UploadingAttachmentMetadata
|
|
||||||
import net.buzzert.kordophone.backend.server.ChatRepository
|
|
||||||
import net.buzzert.kordophonedroid.ui.attachments.AttachmentImageLoader
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
const val MVM_LOG: String = "MessageListViewModel"
|
|
||||||
|
|
||||||
@HiltViewModel
|
|
||||||
class MessageListViewModel @Inject constructor(
|
|
||||||
private val repository: ChatRepository,
|
|
||||||
private val imageLoader: AttachmentImageLoader
|
|
||||||
) : ViewModel()
|
|
||||||
{
|
|
||||||
var conversationGUID: GUID? = null
|
|
||||||
set(value) {
|
|
||||||
field = value
|
|
||||||
value?.let {
|
|
||||||
conversation = repository.conversationForGuid(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var conversation: Conversation? = null
|
|
||||||
private val pendingMessages: MutableStateFlow<List<OutgoingMessage>> = MutableStateFlow(listOf())
|
|
||||||
|
|
||||||
init {
|
|
||||||
// TODO: Need to handle settings changes here!!
|
|
||||||
|
|
||||||
viewModelScope.launch {
|
|
||||||
// Remove pending message after message is delivered.
|
|
||||||
// By now, the repository should've committed this to the store.
|
|
||||||
repository.messageDeliveredChannel.collectLatest { event ->
|
|
||||||
pendingMessages.value =
|
|
||||||
pendingMessages.value.filter { it.guid != event.requestGuid }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val messages: Flow<List<Message>>
|
|
||||||
get() = repository.messagesChanged(conversation!!)
|
|
||||||
.combine(pendingMessages) { a, b -> a.union(b.map { it.asMessage() }) }
|
|
||||||
.map { messages ->
|
|
||||||
messages
|
|
||||||
.sortedBy { it.date }
|
|
||||||
.reversed()
|
|
||||||
}
|
|
||||||
|
|
||||||
val title: String get() = conversation!!.formattedDisplayName()
|
|
||||||
|
|
||||||
val isGroupChat: Boolean get() = conversation!!.isGroupChat
|
|
||||||
|
|
||||||
fun enqueueOutgoingMessage(
|
|
||||||
text: String,
|
|
||||||
attachmentUris: Set<Uri>,
|
|
||||||
context: Context,
|
|
||||||
) {
|
|
||||||
val outgoingMessage = OutgoingMessage(
|
|
||||||
body = text,
|
|
||||||
conversation = conversation!!,
|
|
||||||
attachmentUris = attachmentUris,
|
|
||||||
attachmentDataSource = { uri ->
|
|
||||||
val resolver = context.contentResolver
|
|
||||||
val inputStream = resolver.openInputStream(uri)
|
|
||||||
val mimeType = resolver.getType(uri)
|
|
||||||
|
|
||||||
val extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType) ?: "jpg"
|
|
||||||
val filename = uri.lastPathSegment + ".$extension"
|
|
||||||
|
|
||||||
if (inputStream != null && mimeType != null) {
|
|
||||||
UploadingAttachmentMetadata(
|
|
||||||
inputStream = inputStream,
|
|
||||||
mimeType = mimeType,
|
|
||||||
filename = filename
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
val outgoingGUID = repository.enqueueOutgoingMessage(outgoingMessage)
|
|
||||||
pendingMessages.value = pendingMessages.value + listOf(outgoingMessage)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isPendingMessage(message: Message): Boolean {
|
|
||||||
return pendingMessages.value.any { it.guid == message.guid }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun markAsRead() = viewModelScope.launch {
|
|
||||||
repository.markConversationAsRead(conversation!!)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun synchronize() = viewModelScope.launch {
|
|
||||||
repository.synchronizeConversation(conversation!!, limit = 100)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,226 +0,0 @@
|
|||||||
package net.buzzert.kordophonedroid.ui.settings
|
|
||||||
|
|
||||||
import android.provider.Settings
|
|
||||||
import androidx.annotation.DrawableRes
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.layout.width
|
|
||||||
import androidx.compose.foundation.layout.wrapContentHeight
|
|
||||||
import androidx.compose.foundation.rememberScrollState
|
|
||||||
import androidx.compose.foundation.verticalScroll
|
|
||||||
import androidx.compose.material.Button
|
|
||||||
import androidx.compose.material.Divider
|
|
||||||
import androidx.compose.material.ExperimentalMaterialApi
|
|
||||||
import androidx.compose.material.Icon
|
|
||||||
import androidx.compose.material.MaterialTheme
|
|
||||||
import androidx.compose.material.Scaffold
|
|
||||||
import androidx.compose.material.Surface
|
|
||||||
import androidx.compose.material.Text
|
|
||||||
import androidx.compose.material.TextField
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.State
|
|
||||||
import androidx.compose.runtime.collectAsState
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.res.painterResource
|
|
||||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
|
||||||
import androidx.compose.ui.text.input.TextFieldValue
|
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.compose.ui.window.Dialog
|
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
|
||||||
import net.buzzert.kordophonedroid.R
|
|
||||||
import net.buzzert.kordophonedroid.ui.LocalNavController
|
|
||||||
import net.buzzert.kordophonedroid.ui.theme.KordophoneTopAppBar
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun SettingsScreen(
|
|
||||||
viewModel: SettingsViewModel = hiltViewModel(),
|
|
||||||
) {
|
|
||||||
val navController = LocalNavController.current
|
|
||||||
Scaffold(
|
|
||||||
topBar = {
|
|
||||||
KordophoneTopAppBar(
|
|
||||||
title = "Settings",
|
|
||||||
backAction = { navController.popBackStack() },
|
|
||||||
)
|
|
||||||
},
|
|
||||||
|
|
||||||
) {
|
|
||||||
SettingsFormView(
|
|
||||||
viewModel = viewModel,
|
|
||||||
modifier = Modifier
|
|
||||||
.verticalScroll(rememberScrollState())
|
|
||||||
.padding(it)
|
|
||||||
.padding(6.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun SettingsFormView(
|
|
||||||
viewModel: SettingsViewModel,
|
|
||||||
modifier: Modifier = Modifier
|
|
||||||
) {
|
|
||||||
val serverName = viewModel.serverPreference.collectAsState()
|
|
||||||
val userName = viewModel.usernamePreference.collectAsState()
|
|
||||||
val password = viewModel.passwordPreference.collectAsState()
|
|
||||||
|
|
||||||
Column(modifier) {
|
|
||||||
var serverNameInput by remember { mutableStateOf(TextFieldValue(serverName.value)) }
|
|
||||||
|
|
||||||
SettingsTextField(
|
|
||||||
name = "Server",
|
|
||||||
icon = R.drawable.storage,
|
|
||||||
state = serverName,
|
|
||||||
onSave = { viewModel.saveServerPreference(serverNameInput.text) }
|
|
||||||
) { state ->
|
|
||||||
TextField(serverNameInput, onValueChange = {
|
|
||||||
serverNameInput = it
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
var usernameInput by remember { mutableStateOf(TextFieldValue(userName.value)) }
|
|
||||||
var passwordInput by remember { mutableStateOf(TextFieldValue(password.value)) }
|
|
||||||
SettingsTextField(
|
|
||||||
name = "Authentication",
|
|
||||||
icon = R.drawable.account_circle,
|
|
||||||
state = userName,
|
|
||||||
onSave = {
|
|
||||||
viewModel.saveAuthenticationPreferences(usernameInput.text, passwordInput.text)
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
Column() {
|
|
||||||
TextField(
|
|
||||||
value = usernameInput,
|
|
||||||
onValueChange = { usernameInput = it },
|
|
||||||
label = { Text("Username") },
|
|
||||||
)
|
|
||||||
|
|
||||||
TextField(
|
|
||||||
value = passwordInput,
|
|
||||||
onValueChange = { passwordInput = it },
|
|
||||||
label = {Text("Password") },
|
|
||||||
visualTransformation = PasswordVisualTransformation(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
@OptIn(ExperimentalMaterialApi::class)
|
|
||||||
fun <T> SettingsTextField(
|
|
||||||
name: String,
|
|
||||||
@DrawableRes icon: Int,
|
|
||||||
state: State<T>,
|
|
||||||
onSave: () -> Unit,
|
|
||||||
dialogContent: @Composable (State<T>) -> Unit,
|
|
||||||
) {
|
|
||||||
var showingDialog by remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
if (showingDialog) {
|
|
||||||
Dialog(
|
|
||||||
onDismissRequest = { showingDialog = false }
|
|
||||||
) {
|
|
||||||
EditDialog(
|
|
||||||
name = name,
|
|
||||||
onDismiss = {
|
|
||||||
onSave()
|
|
||||||
showingDialog = false
|
|
||||||
},
|
|
||||||
content = { dialogContent(state) }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Surface(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = 16.dp)
|
|
||||||
,
|
|
||||||
onClick = {
|
|
||||||
showingDialog = true
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
val valueString = state.value.toString().ifEmpty { "(Not set)" }
|
|
||||||
|
|
||||||
Column {
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.Start,
|
|
||||||
modifier = Modifier.padding(vertical = 8.dp)
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
painterResource(id = icon),
|
|
||||||
contentDescription = "",
|
|
||||||
modifier = Modifier.size(24.dp)
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(16.dp))
|
|
||||||
Column(modifier = Modifier.padding(8.dp)) {
|
|
||||||
// Title
|
|
||||||
Text(
|
|
||||||
text = name,
|
|
||||||
style = MaterialTheme.typography.body1,
|
|
||||||
textAlign = TextAlign.Start,
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
|
||||||
|
|
||||||
// Value
|
|
||||||
Text(
|
|
||||||
text = valueString,
|
|
||||||
style = MaterialTheme.typography.body2,
|
|
||||||
textAlign = TextAlign.Start,
|
|
||||||
color = MaterialTheme.colors.onSurface.copy(alpha = 0.5f),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Divider()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun EditDialog(
|
|
||||||
name: String,
|
|
||||||
onDismiss: () -> Unit,
|
|
||||||
content: @Composable () -> Unit,
|
|
||||||
) {
|
|
||||||
Surface() {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.wrapContentHeight()
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(16.dp)
|
|
||||||
) {
|
|
||||||
Text(name)
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
|
|
||||||
content()
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
|
|
||||||
Row {
|
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
|
||||||
Button(onClick = {
|
|
||||||
onDismiss()
|
|
||||||
}) {
|
|
||||||
Text("Save")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
package net.buzzert.kordophonedroid.ui.settings
|
|
||||||
|
|
||||||
import androidx.compose.runtime.MutableState
|
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.asSharedFlow
|
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
|
||||||
import net.buzzert.kordophonedroid.ui.shared.ServerAuthentication
|
|
||||||
import net.buzzert.kordophonedroid.ui.shared.ServerConfigRepository
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@HiltViewModel
|
|
||||||
class SettingsViewModel @Inject constructor(
|
|
||||||
val serverConfigRepository: ServerConfigRepository
|
|
||||||
) : ViewModel() {
|
|
||||||
private val _serverPreference: MutableStateFlow<String> = MutableStateFlow("")
|
|
||||||
var serverPreference = _serverPreference.asStateFlow()
|
|
||||||
|
|
||||||
private val _usernamePreference: MutableStateFlow<String> = MutableStateFlow("")
|
|
||||||
var usernamePreference = _usernamePreference.asStateFlow()
|
|
||||||
|
|
||||||
private val _passwordPreference: MutableStateFlow<String> = MutableStateFlow("")
|
|
||||||
var passwordPreference = _passwordPreference.asStateFlow()
|
|
||||||
|
|
||||||
init {
|
|
||||||
val serverConfig = serverConfigRepository.serverConfig.value
|
|
||||||
serverConfig.serverName?.let { _serverPreference.value = it }
|
|
||||||
serverConfig.authentication?.let {
|
|
||||||
_usernamePreference.value = it.username
|
|
||||||
_passwordPreference.value = it.password
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun saveServerPreference(serverName: String) {
|
|
||||||
_serverPreference.value = serverName
|
|
||||||
|
|
||||||
serverConfigRepository.applyConfig {
|
|
||||||
this.serverName = serverName
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun saveAuthenticationPreferences(username: String, password: String) {
|
|
||||||
_usernamePreference.value = username
|
|
||||||
_passwordPreference.value = password
|
|
||||||
|
|
||||||
serverConfigRepository.applyConfig {
|
|
||||||
this.authentication = ServerAuthentication(username, password)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,114 +0,0 @@
|
|||||||
package net.buzzert.kordophonedroid.ui.shared
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.SharedPreferences
|
|
||||||
import androidx.core.content.edit
|
|
||||||
import androidx.security.crypto.EncryptedSharedPreferences
|
|
||||||
import androidx.security.crypto.MasterKey
|
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
|
||||||
import net.buzzert.kordophone.backend.server.Authentication
|
|
||||||
import java.lang.reflect.Constructor
|
|
||||||
import javax.inject.Inject
|
|
||||||
import javax.inject.Singleton
|
|
||||||
|
|
||||||
|
|
||||||
data class ServerConfig(
|
|
||||||
var serverName: String? = null,
|
|
||||||
var authentication: ServerAuthentication? = null,
|
|
||||||
) {
|
|
||||||
companion object {
|
|
||||||
private const val SHARED_PREF_NAME = "KordophonePreferences"
|
|
||||||
|
|
||||||
fun loadFromSettings(context: Context): ServerConfig {
|
|
||||||
val prefs = getSharedPreferences(context)
|
|
||||||
return ServerConfig(
|
|
||||||
serverName = prefs.getString("serverName", null).normalizedBaseUrl(),
|
|
||||||
authentication = ServerAuthentication.loadFromEncryptedSettings(context)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getSharedPreferences(context: Context): SharedPreferences {
|
|
||||||
return context.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun saveToSettings(context: Context) {
|
|
||||||
val prefs = getSharedPreferences(context)
|
|
||||||
prefs.edit {
|
|
||||||
putString("serverName", serverName.normalizedBaseUrl())
|
|
||||||
apply()
|
|
||||||
}
|
|
||||||
|
|
||||||
authentication?.saveToEncryptedSettings(context)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun String?.normalizedBaseUrl(): String? {
|
|
||||||
val value = this?.trim()?.takeIf { it.isNotEmpty() } ?: return null
|
|
||||||
return if (value.endsWith("/")) value else "$value/"
|
|
||||||
}
|
|
||||||
|
|
||||||
data class ServerAuthentication(
|
|
||||||
val username: String,
|
|
||||||
val password: String,
|
|
||||||
) {
|
|
||||||
companion object {
|
|
||||||
fun loadFromEncryptedSettings(context: Context): ServerAuthentication? {
|
|
||||||
val prefs = getEncryptedSharedPreferences(context)
|
|
||||||
|
|
||||||
val username = prefs.getString("username", null)
|
|
||||||
val password = prefs.getString("password", null)
|
|
||||||
if (username != null && password != null) {
|
|
||||||
return ServerAuthentication(username, password)
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getEncryptedSharedPreferences(context: Context): SharedPreferences {
|
|
||||||
val masterKey = MasterKey.Builder(context)
|
|
||||||
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
return EncryptedSharedPreferences(
|
|
||||||
context,
|
|
||||||
"secrets",
|
|
||||||
masterKey,
|
|
||||||
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
|
|
||||||
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun saveToEncryptedSettings(context: Context) {
|
|
||||||
val prefs = getEncryptedSharedPreferences(context)
|
|
||||||
prefs.edit {
|
|
||||||
putString("username", username)
|
|
||||||
putString("password", password)
|
|
||||||
apply()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun toBackendAuthentication(): Authentication {
|
|
||||||
return Authentication(username, password)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Singleton
|
|
||||||
class ServerConfigRepository @Inject constructor(
|
|
||||||
@ApplicationContext val context: Context,
|
|
||||||
) {
|
|
||||||
// TODO: Initial config should be loaded from device settings.
|
|
||||||
private val _serverConfig = MutableStateFlow(ServerConfig.loadFromSettings(context)) // Initial config
|
|
||||||
val serverConfig: StateFlow<ServerConfig> = _serverConfig
|
|
||||||
|
|
||||||
fun applyConfig(applicator: ServerConfig.() -> Unit) {
|
|
||||||
val config = _serverConfig.value.copy()
|
|
||||||
_serverConfig.value = config.apply(applicator).also {
|
|
||||||
it.serverName = it.serverName.normalizedBaseUrl()
|
|
||||||
}
|
|
||||||
_serverConfig.value.saveToSettings(context)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
package net.buzzert.kordophonedroid.ui.shared
|
|
||||||
|
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
|
||||||
import androidx.compose.ui.text.SpanStyle
|
|
||||||
import androidx.compose.ui.text.buildAnnotatedString
|
|
||||||
import androidx.compose.ui.text.style.TextDecoration
|
|
||||||
|
|
||||||
const val LINK_ANNOTATION_TAG = "link"
|
|
||||||
private val LINK_REGEX = "(https?)://[-a-zA-Z0-9+&@#/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#/%=~_|]".toRegex()
|
|
||||||
|
|
||||||
fun String.linkify(): AnnotatedString {
|
|
||||||
val text = this
|
|
||||||
val matches = LINK_REGEX.findAll(this)
|
|
||||||
return buildAnnotatedString {
|
|
||||||
append(text)
|
|
||||||
|
|
||||||
for (match in matches) {
|
|
||||||
val range = match.range.also {
|
|
||||||
// Make inclusive.
|
|
||||||
IntRange(it.first, it.last + 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Annotate link
|
|
||||||
addStringAnnotation(LINK_ANNOTATION_TAG, match.value, range.first, range.last)
|
|
||||||
|
|
||||||
// Add style
|
|
||||||
addStyle(
|
|
||||||
style = SpanStyle(textDecoration = TextDecoration.Underline),
|
|
||||||
start = range.first, end = range.last
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
package net.buzzert.kordophonedroid.ui.theme
|
|
||||||
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
|
|
||||||
val Purple200 = Color(0xFFBB86FC)
|
|
||||||
val Purple500 = Color(0xFF6200EE)
|
|
||||||
val Purple700 = Color(0xFF3700B3)
|
|
||||||
val Teal200 = Color(0xFF03DAC5)
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
package net.buzzert.kordophonedroid.ui.theme
|
|
||||||
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.material.Shapes
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
|
|
||||||
val Shapes = Shapes(
|
|
||||||
small = RoundedCornerShape(4.dp),
|
|
||||||
medium = RoundedCornerShape(4.dp),
|
|
||||||
large = RoundedCornerShape(0.dp)
|
|
||||||
)
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
package net.buzzert.kordophonedroid.ui.theme
|
|
||||||
|
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
|
||||||
import androidx.compose.animation.slideInVertically
|
|
||||||
import androidx.compose.animation.slideOutVertically
|
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
|
||||||
import androidx.compose.material.Icon
|
|
||||||
import androidx.compose.material.IconButton
|
|
||||||
import androidx.compose.material.MaterialTheme
|
|
||||||
import androidx.compose.material.Text
|
|
||||||
import androidx.compose.material.TopAppBar
|
|
||||||
import androidx.compose.material.darkColors
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.filled.ArrowBack
|
|
||||||
import androidx.compose.material.lightColors
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
|
||||||
|
|
||||||
private val DarkColorPalette = darkColors(
|
|
||||||
primary = Purple200,
|
|
||||||
primaryVariant = Purple700,
|
|
||||||
secondary = Teal200
|
|
||||||
)
|
|
||||||
|
|
||||||
private val LightColorPalette = lightColors(
|
|
||||||
primary = Purple500,
|
|
||||||
primaryVariant = Purple700,
|
|
||||||
secondary = Teal200
|
|
||||||
|
|
||||||
/* Other default colors to override
|
|
||||||
background = Color.White,
|
|
||||||
surface = Color.White,
|
|
||||||
onPrimary = Color.White,
|
|
||||||
onSecondary = Color.Black,
|
|
||||||
onBackground = Color.Black,
|
|
||||||
onSurface = Color.Black,
|
|
||||||
*/
|
|
||||||
)
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun KordophoneTheme(
|
|
||||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
|
||||||
content: @Composable () -> Unit
|
|
||||||
) {
|
|
||||||
val colors = if (darkTheme) {
|
|
||||||
DarkColorPalette
|
|
||||||
} else {
|
|
||||||
LightColorPalette
|
|
||||||
}
|
|
||||||
|
|
||||||
MaterialTheme(
|
|
||||||
colors = colors,
|
|
||||||
typography = Typography,
|
|
||||||
shapes = Shapes,
|
|
||||||
content = content
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun KordophoneTopAppBar(title: String, backAction: () -> Unit, visible: Boolean = true) {
|
|
||||||
AnimatedVisibility(
|
|
||||||
visible = visible,
|
|
||||||
enter = slideInVertically { -it },
|
|
||||||
exit = slideOutVertically { -it },
|
|
||||||
) {
|
|
||||||
TopAppBar(
|
|
||||||
title = {
|
|
||||||
Text(
|
|
||||||
text = title,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
navigationIcon = {
|
|
||||||
IconButton(onClick = backAction) {
|
|
||||||
Icon(Icons.Filled.ArrowBack, null)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
actions = {}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
package net.buzzert.kordophonedroid.ui.theme
|
|
||||||
|
|
||||||
import androidx.compose.material.Typography
|
|
||||||
import androidx.compose.ui.text.TextStyle
|
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.unit.sp
|
|
||||||
|
|
||||||
// Set of Material typography styles to start with
|
|
||||||
val Typography = Typography(
|
|
||||||
body1 = TextStyle(
|
|
||||||
fontFamily = FontFamily.Default,
|
|
||||||
fontWeight = FontWeight.Normal,
|
|
||||||
fontSize = 16.sp
|
|
||||||
)
|
|
||||||
/* Other default text styles to override
|
|
||||||
button = TextStyle(
|
|
||||||
fontFamily = FontFamily.Default,
|
|
||||||
fontWeight = FontWeight.W500,
|
|
||||||
fontSize = 14.sp
|
|
||||||
),
|
|
||||||
caption = TextStyle(
|
|
||||||
fontFamily = FontFamily.Default,
|
|
||||||
fontWeight = FontWeight.Normal,
|
|
||||||
fontSize = 12.sp
|
|
||||||
)
|
|
||||||
*/
|
|
||||||
)
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:aapt="http://schemas.android.com/aapt"
|
|
||||||
android:width="108dp"
|
|
||||||
android:height="108dp"
|
|
||||||
android:viewportWidth="108"
|
|
||||||
android:viewportHeight="108">
|
|
||||||
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
|
|
||||||
<aapt:attr name="android:fillColor">
|
|
||||||
<gradient
|
|
||||||
android:endX="85.84757"
|
|
||||||
android:endY="92.4963"
|
|
||||||
android:startX="42.9492"
|
|
||||||
android:startY="49.59793"
|
|
||||||
android:type="linear">
|
|
||||||
<item
|
|
||||||
android:color="#44000000"
|
|
||||||
android:offset="0.0" />
|
|
||||||
<item
|
|
||||||
android:color="#00000000"
|
|
||||||
android:offset="1.0" />
|
|
||||||
</gradient>
|
|
||||||
</aapt:attr>
|
|
||||||
</path>
|
|
||||||
<path
|
|
||||||
android:fillColor="#FFFFFF"
|
|
||||||
android:fillType="nonZero"
|
|
||||||
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
|
|
||||||
android:strokeWidth="1"
|
|
||||||
android:strokeColor="#00000000" />
|
|
||||||
</vector>
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="24dp"
|
|
||||||
android:height="24dp"
|
|
||||||
android:viewportWidth="960"
|
|
||||||
android:viewportHeight="960"
|
|
||||||
android:tint="?attr/colorControlNormal">
|
|
||||||
<path
|
|
||||||
android:fillColor="@android:color/white"
|
|
||||||
android:pathData="M234,684Q285,645 348,622.5Q411,600 480,600Q549,600 612,622.5Q675,645 726,684Q761,643 780.5,591Q800,539 800,480Q800,347 706.5,253.5Q613,160 480,160Q347,160 253.5,253.5Q160,347 160,480Q160,539 179.5,591Q199,643 234,684ZM480,520Q421,520 380.5,479.5Q340,439 340,380Q340,321 380.5,280.5Q421,240 480,240Q539,240 579.5,280.5Q620,321 620,380Q620,439 579.5,479.5Q539,520 480,520ZM480,880Q397,880 324,848.5Q251,817 197,763Q143,709 111.5,636Q80,563 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,563 848.5,636Q817,709 763,763Q709,817 636,848.5Q563,880 480,880ZM480,800Q533,800 580,784.5Q627,769 666,740Q627,711 580,695.5Q533,680 480,680Q427,680 380,695.5Q333,711 294,740Q333,769 380,784.5Q427,800 480,800ZM480,440Q506,440 523,423Q540,406 540,380Q540,354 523,337Q506,320 480,320Q454,320 437,337Q420,354 420,380Q420,406 437,423Q454,440 480,440ZM480,380Q480,380 480,380Q480,380 480,380Q480,380 480,380Q480,380 480,380Q480,380 480,380Q480,380 480,380Q480,380 480,380Q480,380 480,380ZM480,740Q480,740 480,740Q480,740 480,740Q480,740 480,740Q480,740 480,740Q480,740 480,740Q480,740 480,740Q480,740 480,740Q480,740 480,740Z"/>
|
|
||||||
</vector>
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="24dp"
|
|
||||||
android:height="24dp"
|
|
||||||
android:viewportWidth="960"
|
|
||||||
android:viewportHeight="960"
|
|
||||||
android:tint="?attr/colorControlNormal">
|
|
||||||
<path
|
|
||||||
android:fillColor="@android:color/white"
|
|
||||||
android:pathData="M720,630Q720,734 647,807Q574,880 470,880Q366,880 293,807Q220,734 220,630L220,260Q220,185 272.5,132.5Q325,80 400,80Q475,80 527.5,132.5Q580,185 580,260L580,610Q580,656 548,688Q516,720 470,720Q424,720 392,688Q360,656 360,610L360,240L440,240L440,610Q440,623 448.5,631.5Q457,640 470,640Q483,640 491.5,631.5Q500,623 500,610L500,260Q499,218 470.5,189Q442,160 400,160Q358,160 329,189Q300,218 300,260L300,630Q299,701 349,750.5Q399,800 470,800Q540,800 589,750.5Q638,701 640,630L640,240L720,240L720,630Z"/>
|
|
||||||
</vector>
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="24dp"
|
|
||||||
android:height="24dp"
|
|
||||||
android:viewportWidth="960"
|
|
||||||
android:viewportHeight="960"
|
|
||||||
android:tint="?attr/colorControlNormal">
|
|
||||||
<path
|
|
||||||
android:fillColor="@android:color/white"
|
|
||||||
android:pathData="M480,680Q497,680 508.5,668.5Q520,657 520,640Q520,623 508.5,611.5Q497,600 480,600Q463,600 451.5,611.5Q440,623 440,640Q440,657 451.5,668.5Q463,680 480,680ZM440,520L520,520L520,280L440,280L440,520ZM480,880Q397,880 324,848.5Q251,817 197,763Q143,709 111.5,636Q80,563 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,563 848.5,636Q817,709 763,763Q709,817 636,848.5Q563,880 480,880ZM480,800Q614,800 707,707Q800,614 800,480Q800,346 707,253Q614,160 480,160Q346,160 253,253Q160,346 160,480Q160,614 253,707Q346,800 480,800ZM480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Z"/>
|
|
||||||
</vector>
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
|
|
||||||
<shape
|
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="108dp"
|
|
||||||
android:height="108dp"
|
|
||||||
android:fillColor="@color/black"
|
|
||||||
>
|
|
||||||
<gradient
|
|
||||||
android:startColor="#000"
|
|
||||||
android:endColor="#333"
|
|
||||||
android:angle="1.0"
|
|
||||||
/>
|
|
||||||
</shape>
|
|
||||||
|
Before Width: | Height: | Size: 162 KiB |
|
Before Width: | Height: | Size: 8.4 KiB |
|
Before Width: | Height: | Size: 659 KiB |
@@ -1,10 +0,0 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="24dp"
|
|
||||||
android:height="24dp"
|
|
||||||
android:viewportWidth="960"
|
|
||||||
android:viewportHeight="960"
|
|
||||||
android:tint="?attr/colorControlNormal">
|
|
||||||
<path
|
|
||||||
android:fillColor="@android:color/white"
|
|
||||||
android:pathData="M120,800L120,640L840,640L840,800L120,800ZM200,760L280,760L280,680L200,680L200,760ZM120,320L120,160L840,160L840,320L120,320ZM200,280L280,280L280,200L200,200L200,280ZM120,560L120,400L840,400L840,560L120,560ZM200,520L280,520L280,440L200,440L200,520Z"/>
|
|
||||||
</vector>
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<background android:drawable="@drawable/ic_launcher_background"/>
|
|
||||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
|
||||||
</adaptive-icon>
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<background android:drawable="@drawable/ic_launcher_background"/>
|
|
||||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
|
||||||
</adaptive-icon>
|
|
||||||
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 840 B |
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 6.5 KiB |
|
Before Width: | Height: | Size: 6.8 KiB |
@@ -1,10 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<color name="purple_200">#FFBB86FC</color>
|
|
||||||
<color name="purple_500">#FF6200EE</color>
|
|
||||||
<color name="purple_700">#FF3700B3</color>
|
|
||||||
<color name="teal_200">#FF03DAC5</color>
|
|
||||||
<color name="teal_700">#FF018786</color>
|
|
||||||
<color name="black">#FF000000</color>
|
|
||||||
<color name="white">#FFFFFFFF</color>
|
|
||||||
</resources>
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<color name="ic_launcher_background">#3D3D3D</color>
|
|
||||||
</resources>
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
<resources>
|
|
||||||
<string name="app_name">KordophoneDroid</string>
|
|
||||||
</resources>
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
|
|
||||||
<style name="Theme.KordophoneDroid" parent="android:Theme.Material.Light.NoActionBar">
|
|
||||||
<item name="android:statusBarColor">@color/purple_700</item>
|
|
||||||
</style>
|
|
||||||
</resources>
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?><!--
|
|
||||||
Sample backup rules file; uncomment and customize as necessary.
|
|
||||||
See https://developer.android.com/guide/topics/data/autobackup
|
|
||||||
for details.
|
|
||||||
Note: This file is ignored for devices older that API 31
|
|
||||||
See https://developer.android.com/about/versions/12/backup-restore
|
|
||||||
-->
|
|
||||||
<full-backup-content>
|
|
||||||
<!--
|
|
||||||
<include domain="sharedpref" path="."/>
|
|
||||||
<exclude domain="sharedpref" path="device.xml"/>
|
|
||||||
-->
|
|
||||||
</full-backup-content>
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?><!--
|
|
||||||
Sample data extraction rules file; uncomment and customize as necessary.
|
|
||||||
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
|
|
||||||
for details.
|
|
||||||
-->
|
|
||||||
<data-extraction-rules>
|
|
||||||
<cloud-backup>
|
|
||||||
<!-- TODO: Use <include> and <exclude> to control what is backed up.
|
|
||||||
<include .../>
|
|
||||||
<exclude .../>
|
|
||||||
-->
|
|
||||||
</cloud-backup>
|
|
||||||
<!--
|
|
||||||
<device-transfer>
|
|
||||||
<include .../>
|
|
||||||
<exclude .../>
|
|
||||||
</device-transfer>
|
|
||||||
-->
|
|
||||||
</data-extraction-rules>
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<network-security-config>
|
|
||||||
<domain-config cleartextTrafficPermitted="true">
|
|
||||||
<domain includeSubdomains="true">10.0.2.2</domain>
|
|
||||||
<domain includeSubdomains="true">tesseract.localdomain</domain>
|
|
||||||
<domain includeSubdomains="true">buzzert.kordophone.nor</domain>
|
|
||||||
</domain-config>
|
|
||||||
</network-security-config>
|
|
||||||
1
android/backend/.gitignore
vendored
@@ -1 +0,0 @@
|
|||||||
/build
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
plugins {
|
|
||||||
id 'com.android.library'
|
|
||||||
id 'org.jetbrains.kotlin.android'
|
|
||||||
id 'io.realm.kotlin'
|
|
||||||
}
|
|
||||||
|
|
||||||
android {
|
|
||||||
namespace 'net.buzzert.kordophone.backend'
|
|
||||||
compileSdk 33
|
|
||||||
|
|
||||||
defaultConfig {
|
|
||||||
minSdk 30
|
|
||||||
targetSdk 33
|
|
||||||
|
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
|
||||||
consumerProguardFiles "consumer-rules.pro"
|
|
||||||
}
|
|
||||||
|
|
||||||
buildTypes {
|
|
||||||
release {
|
|
||||||
minifyEnabled false
|
|
||||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
compileOptions {
|
|
||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
|
||||||
targetCompatibility JavaVersion.VERSION_1_8
|
|
||||||
}
|
|
||||||
kotlinOptions {
|
|
||||||
jvmTarget = JavaVersion.VERSION_1_8.toString()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
|
|
||||||
implementation 'androidx.core:core-ktx:1.10.1'
|
|
||||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
|
||||||
implementation 'com.google.android.material:material:1.9.0'
|
|
||||||
implementation 'androidx.core:core-ktx:1.10.1'
|
|
||||||
|
|
||||||
testImplementation 'junit:junit:4.13.2'
|
|
||||||
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
|
|
||||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
|
|
||||||
|
|
||||||
// Third-party
|
|
||||||
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
|
|
||||||
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
|
|
||||||
implementation 'com.google.code.gson:gson:2.9.0'
|
|
||||||
implementation 'com.auth0.android:jwtdecode:2.0.2'
|
|
||||||
|
|
||||||
// Realm
|
|
||||||
implementation "io.realm.kotlin:library-base:${realm_version}"
|
|
||||||
|
|
||||||
// https://mvnrepository.com/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-core
|
|
||||||
implementation group: 'org.jetbrains.kotlinx', name: 'kotlinx-coroutines-core', version: '1.7.3', ext: 'pom'
|
|
||||||
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3'
|
|
||||||
testImplementation 'com.squareup.okhttp3:mockwebserver:4.9.1'
|
|
||||||
}
|
|
||||||
21
android/backend/proguard-rules.pro
vendored
@@ -1,21 +0,0 @@
|
|||||||
# Add project specific ProGuard rules here.
|
|
||||||
# You can control the set of applied configuration files using the
|
|
||||||
# proguardFiles setting in build.gradle.
|
|
||||||
#
|
|
||||||
# For more details, see
|
|
||||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
|
||||||
|
|
||||||
# If your project uses WebView with JS, uncomment the following
|
|
||||||
# and specify the fully qualified class name to the JavaScript interface
|
|
||||||
# class:
|
|
||||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
|
||||||
# public *;
|
|
||||||
#}
|
|
||||||
|
|
||||||
# Uncomment this to preserve the line number information for
|
|
||||||
# debugging stack traces.
|
|
||||||
#-keepattributes SourceFile,LineNumberTable
|
|
||||||
|
|
||||||
# If you keep the line number information, uncomment this to
|
|
||||||
# hide the original source file name.
|
|
||||||
#-renamesourcefileattribute SourceFile
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
package net.buzzert.kordophone.backend
|
|
||||||
|
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
|
||||||
|
|
||||||
import org.junit.Test
|
|
||||||
import org.junit.runner.RunWith
|
|
||||||
|
|
||||||
import org.junit.Assert.*
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Instrumented test, which will execute on an Android device.
|
|
||||||
*
|
|
||||||
* See [testing documentation](http://d.android.com/tools/testing).
|
|
||||||
*/
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
|
||||||
class ExampleInstrumentedTest {
|
|
||||||
@Test
|
|
||||||
fun useAppContext() {
|
|
||||||
// Context of the app under test.
|
|
||||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
|
||||||
assertEquals("net.buzzert.kordophone.backend.test", appContext.packageName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
|
||||||
</manifest>
|
|
||||||
@@ -1,170 +0,0 @@
|
|||||||
package net.buzzert.kordophone.backend.db
|
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import io.realm.kotlin.MutableRealm
|
|
||||||
import io.realm.kotlin.Realm
|
|
||||||
import io.realm.kotlin.RealmConfiguration
|
|
||||||
import io.realm.kotlin.UpdatePolicy
|
|
||||||
import io.realm.kotlin.ext.toRealmList
|
|
||||||
import io.realm.kotlin.query.find
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import kotlinx.coroutines.flow.asFlow
|
|
||||||
import kotlinx.coroutines.flow.map
|
|
||||||
import net.buzzert.kordophone.backend.db.model.Conversation
|
|
||||||
import net.buzzert.kordophone.backend.db.model.Message
|
|
||||||
import net.buzzert.kordophone.backend.db.model.toDatabaseConversation
|
|
||||||
import net.buzzert.kordophone.backend.db.model.toDatabaseMessage
|
|
||||||
import net.buzzert.kordophone.backend.db.model.toRealmInstant
|
|
||||||
import net.buzzert.kordophone.backend.model.GUID
|
|
||||||
import net.buzzert.kordophone.backend.server.REPO_LOG
|
|
||||||
import java.lang.IllegalArgumentException
|
|
||||||
import net.buzzert.kordophone.backend.model.Conversation as ModelConversation
|
|
||||||
import net.buzzert.kordophone.backend.model.Message as ModelMessage
|
|
||||||
|
|
||||||
class CachedChatDatabase (private val realmConfig: RealmConfiguration) {
|
|
||||||
companion object {
|
|
||||||
private val schema = setOf(Conversation::class, Message::class)
|
|
||||||
|
|
||||||
fun liveDatabase(): CachedChatDatabase {
|
|
||||||
return CachedChatDatabase(
|
|
||||||
RealmConfiguration.Builder(schema = schema)
|
|
||||||
.name("chat-cache")
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun testDatabase(): CachedChatDatabase {
|
|
||||||
return CachedChatDatabase(
|
|
||||||
RealmConfiguration.Builder(schema = schema)
|
|
||||||
.name("chat-cache-test")
|
|
||||||
.inMemory()
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val realm = runCatching {
|
|
||||||
Realm.open(realmConfig)
|
|
||||||
}.recover {
|
|
||||||
// We're just a caching layer, so in the event of a migration error, just delete and start over.
|
|
||||||
Log.d(REPO_LOG, "Error opening (${it.message}). Recovering by deleting database.")
|
|
||||||
Realm.deleteRealm(realmConfig)
|
|
||||||
|
|
||||||
return@recover Realm.open(realmConfig)
|
|
||||||
}.getOrThrow()
|
|
||||||
|
|
||||||
// Flow for watching changes to the database
|
|
||||||
val conversationChanges: Flow<List<ModelConversation>>
|
|
||||||
get() = realm.query(Conversation::class).find().asFlow().map {
|
|
||||||
realm.copyFromRealm(it.list)
|
|
||||||
.map { it.toConversation() }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Flow for watching for message changes for a given conversation
|
|
||||||
fun messagesChanged(conversation: ModelConversation): Flow<List<ModelMessage>> {
|
|
||||||
return realm.query(Message::class, "conversationGUID == $0", conversation.guid)
|
|
||||||
.find()
|
|
||||||
.asFlow()
|
|
||||||
.map { it.list.map { it.toMessage(conversation) } }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateConversations(incomingConversations: List<ModelConversation>) = realm.writeBlocking {
|
|
||||||
val incomingDatabaseConversations = incomingConversations.map { it.toDatabaseConversation() }
|
|
||||||
|
|
||||||
var deletedConversations = realm.query(Conversation::class).find()
|
|
||||||
.minus(incomingDatabaseConversations)
|
|
||||||
|
|
||||||
deletedConversations.forEach { conversation ->
|
|
||||||
findLatest(conversation)?.let {
|
|
||||||
delete(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
writeManagedConversations(this, incomingDatabaseConversations)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun writeConversations(conversations: List<ModelConversation>) = realm.writeBlocking {
|
|
||||||
writeManagedConversations(this, conversations.map { it.toDatabaseConversation() })
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun writeManagedConversations(mutableRealm: MutableRealm, conversations: List<Conversation>) {
|
|
||||||
conversations.forEach {conversation ->
|
|
||||||
try {
|
|
||||||
val managedConversation = getManagedConversationByGuid(conversation.guid)
|
|
||||||
mutableRealm.findLatest(managedConversation)?.apply {
|
|
||||||
displayName = conversation.displayName
|
|
||||||
participants = conversation.participants
|
|
||||||
date = conversation.date
|
|
||||||
unreadCount = conversation.unreadCount
|
|
||||||
lastMessagePreview = conversation.lastMessagePreview
|
|
||||||
lastMessageGUID = conversation.lastMessageGUID
|
|
||||||
}
|
|
||||||
} catch (e: NoSuchElementException) {
|
|
||||||
// Conversation does not exist. Copy it to the realm.
|
|
||||||
mutableRealm.copyToRealm(conversation, updatePolicy = UpdatePolicy.ALL)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun deleteConversations(conversations: List<ModelConversation>) = realm.writeBlocking {
|
|
||||||
conversations.forEach { inConversation ->
|
|
||||||
val conversation = getManagedConversationByGuid(inConversation.guid)
|
|
||||||
findLatest(conversation)?.let {
|
|
||||||
delete(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun fetchConversations(): List<ModelConversation> {
|
|
||||||
val itemResults = realm.query(Conversation::class).find()
|
|
||||||
val items = realm.copyFromRealm(itemResults)
|
|
||||||
return items.map { it.toConversation() }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun writeMessages(messages: List<ModelMessage>, conversation: ModelConversation, outgoing: Boolean = false) {
|
|
||||||
if (messages.isEmpty()) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val dbConversation = getManagedConversationByGuid(conversation.guid)
|
|
||||||
realm.writeBlocking {
|
|
||||||
messages
|
|
||||||
.map { it.toDatabaseMessage(outgoing = outgoing) }
|
|
||||||
.map { copyToRealm(it, updatePolicy = UpdatePolicy.ALL) }
|
|
||||||
|
|
||||||
findLatest(dbConversation)?.let {
|
|
||||||
val lastMessage = messages.maxByOrNull { it.date }!!
|
|
||||||
|
|
||||||
val lastMessageDate = lastMessage.date.toInstant().toRealmInstant()
|
|
||||||
if (lastMessageDate > it.date) {
|
|
||||||
it.lastMessageGUID = lastMessage.guid
|
|
||||||
it.lastMessagePreview = lastMessage.displayText
|
|
||||||
|
|
||||||
// This will cause sort order to change. I think this ends
|
|
||||||
// up getting updated whenever we get conversation changes anyway.
|
|
||||||
// it.date = lastMessageDate
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun fetchMessages(conversation: ModelConversation): List<ModelMessage> {
|
|
||||||
return realm.query(Message::class, "conversationGUID == $0", conversation.guid)
|
|
||||||
.find()
|
|
||||||
.map { it.toMessage(conversation) }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun close() {
|
|
||||||
realm.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getManagedConversationByGuid(guid: GUID): Conversation {
|
|
||||||
return realm.query(Conversation::class, "guid == $0", guid)
|
|
||||||
.find()
|
|
||||||
.first()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getConversationByGuid(guid: GUID): Conversation {
|
|
||||||
return realm.copyFromRealm(getManagedConversationByGuid(guid))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
package net.buzzert.kordophone.backend.db.model
|
|
||||||
|
|
||||||
import io.realm.kotlin.ext.realmListOf
|
|
||||||
import io.realm.kotlin.ext.toRealmList
|
|
||||||
import io.realm.kotlin.types.RealmInstant
|
|
||||||
import io.realm.kotlin.types.RealmList
|
|
||||||
import io.realm.kotlin.types.RealmObject
|
|
||||||
import io.realm.kotlin.types.annotations.PrimaryKey
|
|
||||||
import net.buzzert.kordophone.backend.model.GUID
|
|
||||||
import org.mongodb.kbson.ObjectId
|
|
||||||
import net.buzzert.kordophone.backend.model.Conversation as ModelConversation
|
|
||||||
import java.util.Date
|
|
||||||
|
|
||||||
open class Conversation(
|
|
||||||
@PrimaryKey
|
|
||||||
var guid: GUID,
|
|
||||||
|
|
||||||
var displayName: String?,
|
|
||||||
var participants: RealmList<String>,
|
|
||||||
var date: RealmInstant,
|
|
||||||
var unreadCount: Int,
|
|
||||||
|
|
||||||
var lastMessageGUID: String?,
|
|
||||||
var lastMessagePreview: String?,
|
|
||||||
): RealmObject
|
|
||||||
{
|
|
||||||
constructor() : this(
|
|
||||||
guid = ObjectId().toString(),
|
|
||||||
|
|
||||||
displayName = null,
|
|
||||||
participants = realmListOf<String>(),
|
|
||||||
date = RealmInstant.now(),
|
|
||||||
unreadCount = 0,
|
|
||||||
lastMessagePreview = null,
|
|
||||||
lastMessageGUID = null,
|
|
||||||
)
|
|
||||||
|
|
||||||
fun toConversation(): ModelConversation {
|
|
||||||
return ModelConversation(
|
|
||||||
displayName = displayName,
|
|
||||||
participants = participants.toList(),
|
|
||||||
date = Date.from(date.toInstant()),
|
|
||||||
unreadCount = unreadCount,
|
|
||||||
guid = guid,
|
|
||||||
lastMessagePreview = lastMessagePreview,
|
|
||||||
lastMessage = null,
|
|
||||||
lastFetchedMessageGUID = lastMessageGUID
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
|
||||||
if (other == null || javaClass != other.javaClass) return false
|
|
||||||
|
|
||||||
val o = other as Conversation
|
|
||||||
return guid == o.guid
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
|
||||||
return guid.hashCode()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun ModelConversation.toDatabaseConversation(): Conversation {
|
|
||||||
val from = this
|
|
||||||
return Conversation().apply {
|
|
||||||
displayName = from.displayName
|
|
||||||
participants = from.participants.toRealmList()
|
|
||||||
date = from.date.toInstant().toRealmInstant()
|
|
||||||
unreadCount = from.unreadCount
|
|
||||||
lastMessagePreview = from.lastMessagePreview
|
|
||||||
lastMessageGUID = from.lastFetchedMessageGUID
|
|
||||||
guid = from.guid
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
package net.buzzert.kordophone.backend.db.model
|
|
||||||
|
|
||||||
import io.realm.kotlin.types.RealmInstant
|
|
||||||
import java.time.Instant
|
|
||||||
|
|
||||||
// Copied from Realm's documentation
|
|
||||||
// https://www.mongodb.com/docs/realm/sdk/kotlin/realm-database/schemas/supported-types/
|
|
||||||
|
|
||||||
fun RealmInstant.toInstant(): Instant {
|
|
||||||
val sec: Long = this.epochSeconds
|
|
||||||
// The value always lies in the range `-999_999_999..999_999_999`.
|
|
||||||
// minus for timestamps before epoch, positive for after
|
|
||||||
val nano: Int = this.nanosecondsOfSecond
|
|
||||||
return if (sec >= 0) { // For positive timestamps, conversion can happen directly
|
|
||||||
Instant.ofEpochSecond(sec, nano.toLong())
|
|
||||||
} else {
|
|
||||||
// For negative timestamps, RealmInstant starts from the higher value with negative
|
|
||||||
// nanoseconds, while Instant starts from the lower value with positive nanoseconds
|
|
||||||
// TODO This probably breaks at edge cases like MIN/MAX
|
|
||||||
Instant.ofEpochSecond(sec - 1, 1_000_000 + nano.toLong())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Instant.toRealmInstant(): RealmInstant {
|
|
||||||
val sec: Long = this.epochSecond
|
|
||||||
// The value is always positive and lies in the range `0..999_999_999`.
|
|
||||||
val nano: Int = this.nano
|
|
||||||
|
|
||||||
return if (sec >= 0) { // For positive timestamps, conversion can happen directly
|
|
||||||
RealmInstant.from(sec, nano)
|
|
||||||
} else {
|
|
||||||
// For negative timestamps, RealmInstant starts from the higher value with negative
|
|
||||||
// nanoseconds, while Instant starts from the lower value with positive nanoseconds
|
|
||||||
// TODO This probably breaks at edge cases like MIN/MAX
|
|
||||||
RealmInstant.from(sec + 1, -1_000_000 + nano)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
package net.buzzert.kordophone.backend.db.model
|
|
||||||
|
|
||||||
import android.view.Display.Mode
|
|
||||||
import io.realm.kotlin.Realm
|
|
||||||
import io.realm.kotlin.ext.realmListOf
|
|
||||||
import io.realm.kotlin.ext.toRealmList
|
|
||||||
import io.realm.kotlin.types.EmbeddedRealmObject
|
|
||||||
import io.realm.kotlin.types.RealmInstant
|
|
||||||
import io.realm.kotlin.types.RealmList
|
|
||||||
import io.realm.kotlin.types.RealmObject
|
|
||||||
import io.realm.kotlin.types.annotations.PrimaryKey
|
|
||||||
import net.buzzert.kordophone.backend.db.model.Conversation
|
|
||||||
import net.buzzert.kordophone.backend.model.GUID
|
|
||||||
import org.mongodb.kbson.ObjectId
|
|
||||||
import net.buzzert.kordophone.backend.model.Message as ModelMessage
|
|
||||||
import net.buzzert.kordophone.backend.model.Conversation as ModelConversation
|
|
||||||
import java.util.Date
|
|
||||||
|
|
||||||
open class Message(
|
|
||||||
@PrimaryKey
|
|
||||||
var guid: GUID,
|
|
||||||
|
|
||||||
var text: String,
|
|
||||||
var sender: String?,
|
|
||||||
var date: RealmInstant,
|
|
||||||
var attachmentGUIDs: RealmList<String>,
|
|
||||||
|
|
||||||
var conversationGUID: GUID,
|
|
||||||
): RealmObject
|
|
||||||
{
|
|
||||||
constructor() : this(
|
|
||||||
guid = ObjectId().toString(),
|
|
||||||
text = "",
|
|
||||||
sender = null,
|
|
||||||
date = RealmInstant.now(),
|
|
||||||
attachmentGUIDs = realmListOf<String>(),
|
|
||||||
conversationGUID = ObjectId().toString(),
|
|
||||||
)
|
|
||||||
|
|
||||||
fun toMessage(parentConversation: ModelConversation): ModelMessage {
|
|
||||||
return ModelMessage(
|
|
||||||
text = text,
|
|
||||||
guid = guid,
|
|
||||||
sender = sender,
|
|
||||||
date = Date.from(date.toInstant()),
|
|
||||||
attachmentGUIDs = attachmentGUIDs.toList(),
|
|
||||||
conversation = parentConversation,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun ModelMessage.toDatabaseMessage(outgoing: Boolean = false): Message {
|
|
||||||
val from = this
|
|
||||||
return Message().apply {
|
|
||||||
text = from.text
|
|
||||||
guid = from.guid
|
|
||||||
sender = from.sender
|
|
||||||
date = from.date.toInstant().toRealmInstant()
|
|
||||||
conversationGUID = from.conversation.guid
|
|
||||||
from.attachmentGUIDs?.let {
|
|
||||||
attachmentGUIDs = it.toRealmList()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||