diff --git a/QuickLocation.xcodeproj/project.pbxproj b/QuickLocation.xcodeproj/project.pbxproj index 966e7b9f..6b222ad7 100644 --- a/QuickLocation.xcodeproj/project.pbxproj +++ b/QuickLocation.xcodeproj/project.pbxproj @@ -332,6 +332,9 @@ 55B218A13024A00100784708 /* CancelAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B218A13024A00100784707 /* CancelAccountView.swift */; }; 55B218A13024A0010078470A /* MemberInfoVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B218A13024A00100784709 /* MemberInfoVC.swift */; }; 55B218A13024A0010078470C /* MemberInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B218A13024A0010078470B /* MemberInfoView.swift */; }; + 55B233013035000100785041 /* GenderSelectPopVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B233003035000100785040 /* GenderSelectPopVC.swift */; }; + 55B234013036000100785051 /* RelationSelectPopVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B234003036000100785050 /* RelationSelectPopVC.swift */; }; + 55B234033036000100785053 /* RelationStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B234023036000100785052 /* RelationStore.swift */; }; 55B218B13024B00100784722 /* FeatureIntroVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B218B13024B00100784721 /* FeatureIntroVC.swift */; }; 55B218B13024B00100784724 /* FeatureIntroView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B218B13024B00100784723 /* FeatureIntroView.swift */; }; 55B219A2302A000100784751 /* UnlockRequestPopView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B219A1302A000100784750 /* UnlockRequestPopView.swift */; }; @@ -770,6 +773,9 @@ 55B218A13024A00100784707 /* CancelAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CancelAccountView.swift; sourceTree = ""; }; 55B218A13024A00100784709 /* MemberInfoVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemberInfoVC.swift; sourceTree = ""; }; 55B218A13024A0010078470B /* MemberInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemberInfoView.swift; sourceTree = ""; }; + 55B233003035000100785040 /* GenderSelectPopVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenderSelectPopVC.swift; sourceTree = ""; }; + 55B234003036000100785050 /* RelationSelectPopVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelationSelectPopVC.swift; sourceTree = ""; }; + 55B234023036000100785052 /* RelationStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelationStore.swift; sourceTree = ""; }; 55B218B13024B00100784721 /* FeatureIntroVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureIntroVC.swift; sourceTree = ""; }; 55B218B13024B00100784723 /* FeatureIntroView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureIntroView.swift; sourceTree = ""; }; 55B219A1302A000100784750 /* UnlockRequestPopView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnlockRequestPopView.swift; sourceTree = ""; }; @@ -1140,6 +1146,7 @@ children = ( 305A75172FCA8C7000227D26 /* Account.swift */, 305A75182FCA8C7000227D26 /* AppContextManager.swift */, + 55B234023036000100785052 /* RelationStore.swift */, 305A75192FCA8C7000227D26 /* UserConfigModel.swift */, 305A751A2FCA8C7000227D26 /* UserConfigResponse.swift */, ); @@ -2058,6 +2065,8 @@ children = ( 55B218A13024A00100784709 /* MemberInfoVC.swift */, 55B218A13024A0010078470B /* MemberInfoView.swift */, + 55B233003035000100785040 /* GenderSelectPopVC.swift */, + 55B234003036000100785050 /* RelationSelectPopVC.swift */, ); path = MemberInfo; sourceTree = ""; @@ -2516,6 +2525,9 @@ 55B218A13024A00100784708 /* CancelAccountView.swift in Sources */, 55B218A13024A0010078470A /* MemberInfoVC.swift in Sources */, 55B218A13024A0010078470C /* MemberInfoView.swift in Sources */, + 55B233013035000100785041 /* GenderSelectPopVC.swift in Sources */, + 55B234013036000100785051 /* RelationSelectPopVC.swift in Sources */, + 55B234033036000100785053 /* RelationStore.swift in Sources */, 55B220013031000100784801 /* FeedbackVC.swift in Sources */, 55B220033031000100784803 /* FeedbackView.swift in Sources */, 55B220053031000100784805 /* FeedbackSuccessView.swift in Sources */, diff --git a/QuickLocation/API/PigeonAPI.swift b/QuickLocation/API/PigeonAPI.swift index f389a199..03b623a1 100644 --- a/QuickLocation/API/PigeonAPI.swift +++ b/QuickLocation/API/PigeonAPI.swift @@ -11,11 +11,19 @@ enum PigeonAPI { /// - Parameters: /// - groupKey: 圈子 key /// - toUsers: 收件人加密 id - /// - msgType: 1 图片 2 语音 - /// - msg: 上传返回的 file id - /// - textMsg: 留言文本 + /// - msgType: 2 语音 3 文案 + /// - msg: 文案留言,或语音 file id + /// - bgImg: 自定义背景图 file id;图片模版不传 /// - expireTime: 过期秒数,0 表示服务端默认 7 天 - case send(groupKey: String, toUsers: [String], msgType: Int, msg: String, textMsg: String, expireTime: Int) + case send( + groupKey: String, + toUsers: [String], + msgType: Int, + msg: String, + bgImg: String, + expireTime: Int, + extra: [String: Any]? + ) /// 已发送的传书记录 case sent(groupKey: String) /// 收件箱 @@ -49,14 +57,17 @@ extension PigeonAPI: MultiTargetProtocol { var task: Moya.Task { switch self { - case let .send(groupKey, toUsers, msgType, msg, textMsg, expireTime): + case let .send(groupKey, toUsers, msgType, msg, bgImg, expireTime, extra): var params = Parameters() params["group_key"] = groupKey params["to_user"] = toUsers params["msg_type"] = msgType params["msg"] = msg - params["text_msg"] = textMsg + params["bg_img"] = bgImg.isEmpty ? 0 : bgImg params["expire_time"] = expireTime + if let extra { + params["extra"] = extra + } return .requestParameters(parameters: params, encoding: JSONEncoding()) case let .sent(groupKey): var params = Parameters() diff --git a/QuickLocation/API/UserAPI.swift b/QuickLocation/API/UserAPI.swift index f2405ec7..03cd3383 100644 --- a/QuickLocation/API/UserAPI.swift +++ b/QuickLocation/API/UserAPI.swift @@ -83,6 +83,9 @@ enum UserAPI { /// 紧急人邮箱 case setEmail(email: String) + + /// 关系列表 + case relations } extension UserAPI: MultiTargetProtocol { @@ -125,12 +128,14 @@ extension UserAPI: MultiTargetProtocol { return "mapi/bubble/operate" case .setEmail: return "mapi/user/signin/setemail" + case .relations: + return "mapi/user/relations" } } var method: Moya.Method { switch self { - case .userInfo, .userStatus, .signInInfo, .notice, .followList: + case .userInfo, .userStatus, .signInInfo, .notice, .followList, .relations: return .get case .changePhone, .setGender: return .put @@ -229,6 +234,9 @@ extension UserAPI: MultiTargetProtocol { var params = Parameters() params["email"] = email return .requestParameters(parameters: params, encoding: JSONEncoding()) + + case .relations: + return .requestPlain } } } diff --git a/QuickLocation/Assets.xcassets/Home/report_bolt.imageset/Contents.json b/QuickLocation/Assets.xcassets/Home/report_bolt.imageset/Contents.json new file mode 100644 index 00000000..3dc11048 --- /dev/null +++ b/QuickLocation/Assets.xcassets/Home/report_bolt.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "idiom" : "universal", "scale" : "1x" }, + { "filename" : "report_bolt@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "report_bolt@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info" : { "author" : "xcode", "version" : 1 } +} diff --git a/QuickLocation/Assets.xcassets/Home/report_bolt.imageset/report_bolt@2x.png b/QuickLocation/Assets.xcassets/Home/report_bolt.imageset/report_bolt@2x.png new file mode 100644 index 00000000..f002f15d Binary files /dev/null and b/QuickLocation/Assets.xcassets/Home/report_bolt.imageset/report_bolt@2x.png differ diff --git a/QuickLocation/Assets.xcassets/Home/report_bolt.imageset/report_bolt@3x.png b/QuickLocation/Assets.xcassets/Home/report_bolt.imageset/report_bolt@3x.png new file mode 100644 index 00000000..1becc029 Binary files /dev/null and b/QuickLocation/Assets.xcassets/Home/report_bolt.imageset/report_bolt@3x.png differ diff --git a/QuickLocation/Assets.xcassets/Home/report_phone.imageset/Contents.json b/QuickLocation/Assets.xcassets/Home/report_phone.imageset/Contents.json new file mode 100644 index 00000000..82ab8c87 --- /dev/null +++ b/QuickLocation/Assets.xcassets/Home/report_phone.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "idiom" : "universal", "scale" : "1x" }, + { "filename" : "report_phone@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "report_phone@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info" : { "author" : "xcode", "version" : 1 } +} diff --git a/QuickLocation/Assets.xcassets/Home/report_phone.imageset/report_phone@2x.png b/QuickLocation/Assets.xcassets/Home/report_phone.imageset/report_phone@2x.png new file mode 100644 index 00000000..196320e9 Binary files /dev/null and b/QuickLocation/Assets.xcassets/Home/report_phone.imageset/report_phone@2x.png differ diff --git a/QuickLocation/Assets.xcassets/Home/report_phone.imageset/report_phone@3x.png b/QuickLocation/Assets.xcassets/Home/report_phone.imageset/report_phone@3x.png new file mode 100644 index 00000000..79e89576 Binary files /dev/null and b/QuickLocation/Assets.xcassets/Home/report_phone.imageset/report_phone@3x.png differ diff --git a/QuickLocation/Assets.xcassets/Home/report_screen.imageset/Contents.json b/QuickLocation/Assets.xcassets/Home/report_screen.imageset/Contents.json new file mode 100644 index 00000000..d96722fb --- /dev/null +++ b/QuickLocation/Assets.xcassets/Home/report_screen.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "idiom" : "universal", "scale" : "1x" }, + { "filename" : "report_screen@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "report_screen@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info" : { "author" : "xcode", "version" : 1 } +} diff --git a/QuickLocation/Assets.xcassets/Home/report_screen.imageset/report_screen@2x.png b/QuickLocation/Assets.xcassets/Home/report_screen.imageset/report_screen@2x.png new file mode 100644 index 00000000..fae27a3e Binary files /dev/null and b/QuickLocation/Assets.xcassets/Home/report_screen.imageset/report_screen@2x.png differ diff --git a/QuickLocation/Assets.xcassets/Home/report_screen.imageset/report_screen@3x.png b/QuickLocation/Assets.xcassets/Home/report_screen.imageset/report_screen@3x.png new file mode 100644 index 00000000..82882996 Binary files /dev/null and b/QuickLocation/Assets.xcassets/Home/report_screen.imageset/report_screen@3x.png differ diff --git a/QuickLocation/Assets.xcassets/Home/report_usage.imageset/Contents.json b/QuickLocation/Assets.xcassets/Home/report_usage.imageset/Contents.json new file mode 100644 index 00000000..61feb918 --- /dev/null +++ b/QuickLocation/Assets.xcassets/Home/report_usage.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "idiom" : "universal", "scale" : "1x" }, + { "filename" : "report_usage@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "report_usage@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info" : { "author" : "xcode", "version" : 1 } +} diff --git a/QuickLocation/Assets.xcassets/Home/report_usage.imageset/report_usage@2x.png b/QuickLocation/Assets.xcassets/Home/report_usage.imageset/report_usage@2x.png new file mode 100644 index 00000000..0ed245ba Binary files /dev/null and b/QuickLocation/Assets.xcassets/Home/report_usage.imageset/report_usage@2x.png differ diff --git a/QuickLocation/Assets.xcassets/Home/report_usage.imageset/report_usage@3x.png b/QuickLocation/Assets.xcassets/Home/report_usage.imageset/report_usage@3x.png new file mode 100644 index 00000000..4733e8d0 Binary files /dev/null and b/QuickLocation/Assets.xcassets/Home/report_usage.imageset/report_usage@3x.png differ diff --git a/QuickLocation/Assets.xcassets/MemberInfo/Contents.json b/QuickLocation/Assets.xcassets/MemberInfo/Contents.json new file mode 100644 index 00000000..a162e38a --- /dev/null +++ b/QuickLocation/Assets.xcassets/MemberInfo/Contents.json @@ -0,0 +1,4 @@ +{ + "info" : { "author" : "xcode", "version" : 1 }, + "properties" : { "provides-namespace" : true } +} diff --git a/QuickLocation/Assets.xcassets/MemberInfo/close_circle.imageset/Contents.json b/QuickLocation/Assets.xcassets/MemberInfo/close_circle.imageset/Contents.json new file mode 100644 index 00000000..f7be4c7b --- /dev/null +++ b/QuickLocation/Assets.xcassets/MemberInfo/close_circle.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "idiom" : "universal", "scale" : "1x" }, + { "filename" : "close_circle@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "close_circle@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info" : { "author" : "xcode", "version" : 1 } +} diff --git a/QuickLocation/Assets.xcassets/MemberInfo/close_circle.imageset/close_circle@2x.png b/QuickLocation/Assets.xcassets/MemberInfo/close_circle.imageset/close_circle@2x.png new file mode 100644 index 00000000..e19ce065 Binary files /dev/null and b/QuickLocation/Assets.xcassets/MemberInfo/close_circle.imageset/close_circle@2x.png differ diff --git a/QuickLocation/Assets.xcassets/MemberInfo/close_circle.imageset/close_circle@3x.png b/QuickLocation/Assets.xcassets/MemberInfo/close_circle.imageset/close_circle@3x.png new file mode 100644 index 00000000..4602de67 Binary files /dev/null and b/QuickLocation/Assets.xcassets/MemberInfo/close_circle.imageset/close_circle@3x.png differ diff --git a/QuickLocation/Assets.xcassets/MemberInfo/gender_female_off.imageset/Contents.json b/QuickLocation/Assets.xcassets/MemberInfo/gender_female_off.imageset/Contents.json new file mode 100644 index 00000000..34e514e2 --- /dev/null +++ b/QuickLocation/Assets.xcassets/MemberInfo/gender_female_off.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "idiom" : "universal", "scale" : "1x" }, + { "filename" : "gender_female_off@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "gender_female_off@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info" : { "author" : "xcode", "version" : 1 } +} diff --git a/QuickLocation/Assets.xcassets/MemberInfo/gender_female_off.imageset/gender_female_off@2x.png b/QuickLocation/Assets.xcassets/MemberInfo/gender_female_off.imageset/gender_female_off@2x.png new file mode 100644 index 00000000..f7369a5d Binary files /dev/null and b/QuickLocation/Assets.xcassets/MemberInfo/gender_female_off.imageset/gender_female_off@2x.png differ diff --git a/QuickLocation/Assets.xcassets/MemberInfo/gender_female_off.imageset/gender_female_off@3x.png b/QuickLocation/Assets.xcassets/MemberInfo/gender_female_off.imageset/gender_female_off@3x.png new file mode 100644 index 00000000..f1fb1926 Binary files /dev/null and b/QuickLocation/Assets.xcassets/MemberInfo/gender_female_off.imageset/gender_female_off@3x.png differ diff --git a/QuickLocation/Assets.xcassets/MemberInfo/gender_female_on.imageset/Contents.json b/QuickLocation/Assets.xcassets/MemberInfo/gender_female_on.imageset/Contents.json new file mode 100644 index 00000000..1da325cd --- /dev/null +++ b/QuickLocation/Assets.xcassets/MemberInfo/gender_female_on.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "idiom" : "universal", "scale" : "1x" }, + { "filename" : "gender_female_on@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "gender_female_on@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info" : { "author" : "xcode", "version" : 1 } +} diff --git a/QuickLocation/Assets.xcassets/MemberInfo/gender_female_on.imageset/gender_female_on@2x.png b/QuickLocation/Assets.xcassets/MemberInfo/gender_female_on.imageset/gender_female_on@2x.png new file mode 100644 index 00000000..170680c1 Binary files /dev/null and b/QuickLocation/Assets.xcassets/MemberInfo/gender_female_on.imageset/gender_female_on@2x.png differ diff --git a/QuickLocation/Assets.xcassets/MemberInfo/gender_female_on.imageset/gender_female_on@3x.png b/QuickLocation/Assets.xcassets/MemberInfo/gender_female_on.imageset/gender_female_on@3x.png new file mode 100644 index 00000000..521c715f Binary files /dev/null and b/QuickLocation/Assets.xcassets/MemberInfo/gender_female_on.imageset/gender_female_on@3x.png differ diff --git a/QuickLocation/Assets.xcassets/MemberInfo/gender_male_off.imageset/Contents.json b/QuickLocation/Assets.xcassets/MemberInfo/gender_male_off.imageset/Contents.json new file mode 100644 index 00000000..98920140 --- /dev/null +++ b/QuickLocation/Assets.xcassets/MemberInfo/gender_male_off.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "idiom" : "universal", "scale" : "1x" }, + { "filename" : "gender_male_off@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "gender_male_off@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info" : { "author" : "xcode", "version" : 1 } +} diff --git a/QuickLocation/Assets.xcassets/MemberInfo/gender_male_off.imageset/gender_male_off@2x.png b/QuickLocation/Assets.xcassets/MemberInfo/gender_male_off.imageset/gender_male_off@2x.png new file mode 100644 index 00000000..fc1fde7c Binary files /dev/null and b/QuickLocation/Assets.xcassets/MemberInfo/gender_male_off.imageset/gender_male_off@2x.png differ diff --git a/QuickLocation/Assets.xcassets/MemberInfo/gender_male_off.imageset/gender_male_off@3x.png b/QuickLocation/Assets.xcassets/MemberInfo/gender_male_off.imageset/gender_male_off@3x.png new file mode 100644 index 00000000..a16bfb29 Binary files /dev/null and b/QuickLocation/Assets.xcassets/MemberInfo/gender_male_off.imageset/gender_male_off@3x.png differ diff --git a/QuickLocation/Assets.xcassets/MemberInfo/gender_male_on.imageset/Contents.json b/QuickLocation/Assets.xcassets/MemberInfo/gender_male_on.imageset/Contents.json new file mode 100644 index 00000000..bd4b23b2 --- /dev/null +++ b/QuickLocation/Assets.xcassets/MemberInfo/gender_male_on.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "idiom" : "universal", "scale" : "1x" }, + { "filename" : "gender_male_on@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "gender_male_on@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info" : { "author" : "xcode", "version" : 1 } +} diff --git a/QuickLocation/Assets.xcassets/MemberInfo/gender_male_on.imageset/gender_male_on@2x.png b/QuickLocation/Assets.xcassets/MemberInfo/gender_male_on.imageset/gender_male_on@2x.png new file mode 100644 index 00000000..2da42a51 Binary files /dev/null and b/QuickLocation/Assets.xcassets/MemberInfo/gender_male_on.imageset/gender_male_on@2x.png differ diff --git a/QuickLocation/Assets.xcassets/MemberInfo/gender_male_on.imageset/gender_male_on@3x.png b/QuickLocation/Assets.xcassets/MemberInfo/gender_male_on.imageset/gender_male_on@3x.png new file mode 100644 index 00000000..0d184184 Binary files /dev/null and b/QuickLocation/Assets.xcassets/MemberInfo/gender_male_on.imageset/gender_male_on@3x.png differ diff --git a/QuickLocation/Assets.xcassets/MemberInfo/gender_secret_off.imageset/Contents.json b/QuickLocation/Assets.xcassets/MemberInfo/gender_secret_off.imageset/Contents.json new file mode 100644 index 00000000..208e05da --- /dev/null +++ b/QuickLocation/Assets.xcassets/MemberInfo/gender_secret_off.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "idiom" : "universal", "scale" : "1x" }, + { "filename" : "gender_secret_off@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "gender_secret_off@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info" : { "author" : "xcode", "version" : 1 } +} diff --git a/QuickLocation/Assets.xcassets/MemberInfo/gender_secret_off.imageset/gender_secret_off@2x.png b/QuickLocation/Assets.xcassets/MemberInfo/gender_secret_off.imageset/gender_secret_off@2x.png new file mode 100644 index 00000000..8665d485 Binary files /dev/null and b/QuickLocation/Assets.xcassets/MemberInfo/gender_secret_off.imageset/gender_secret_off@2x.png differ diff --git a/QuickLocation/Assets.xcassets/MemberInfo/gender_secret_off.imageset/gender_secret_off@3x.png b/QuickLocation/Assets.xcassets/MemberInfo/gender_secret_off.imageset/gender_secret_off@3x.png new file mode 100644 index 00000000..c58ac327 Binary files /dev/null and b/QuickLocation/Assets.xcassets/MemberInfo/gender_secret_off.imageset/gender_secret_off@3x.png differ diff --git a/QuickLocation/Assets.xcassets/MemberInfo/gender_secret_on.imageset/Contents.json b/QuickLocation/Assets.xcassets/MemberInfo/gender_secret_on.imageset/Contents.json new file mode 100644 index 00000000..c185a712 --- /dev/null +++ b/QuickLocation/Assets.xcassets/MemberInfo/gender_secret_on.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "idiom" : "universal", "scale" : "1x" }, + { "filename" : "gender_secret_on@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "gender_secret_on@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info" : { "author" : "xcode", "version" : 1 } +} diff --git a/QuickLocation/Assets.xcassets/MemberInfo/gender_secret_on.imageset/gender_secret_on@2x.png b/QuickLocation/Assets.xcassets/MemberInfo/gender_secret_on.imageset/gender_secret_on@2x.png new file mode 100644 index 00000000..a96fc9c4 Binary files /dev/null and b/QuickLocation/Assets.xcassets/MemberInfo/gender_secret_on.imageset/gender_secret_on@2x.png differ diff --git a/QuickLocation/Assets.xcassets/MemberInfo/gender_secret_on.imageset/gender_secret_on@3x.png b/QuickLocation/Assets.xcassets/MemberInfo/gender_secret_on.imageset/gender_secret_on@3x.png new file mode 100644 index 00000000..82277ce3 Binary files /dev/null and b/QuickLocation/Assets.xcassets/MemberInfo/gender_secret_on.imageset/gender_secret_on@3x.png differ diff --git a/QuickLocation/Assets.xcassets/Mine/circle_days_pair.imageset/Contents.json b/QuickLocation/Assets.xcassets/Mine/circle_days_pair.imageset/Contents.json new file mode 100644 index 00000000..5df7ea21 --- /dev/null +++ b/QuickLocation/Assets.xcassets/Mine/circle_days_pair.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "idiom" : "universal", "scale" : "1x" }, + { "filename" : "circle_days_pair@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "circle_days_pair@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info" : { "author" : "xcode", "version" : 1 } +} diff --git a/QuickLocation/Assets.xcassets/Mine/circle_days_pair.imageset/circle_days_pair@2x.png b/QuickLocation/Assets.xcassets/Mine/circle_days_pair.imageset/circle_days_pair@2x.png new file mode 100644 index 00000000..f0cab7b1 Binary files /dev/null and b/QuickLocation/Assets.xcassets/Mine/circle_days_pair.imageset/circle_days_pair@2x.png differ diff --git a/QuickLocation/Assets.xcassets/Mine/circle_days_pair.imageset/circle_days_pair@3x.png b/QuickLocation/Assets.xcassets/Mine/circle_days_pair.imageset/circle_days_pair@3x.png new file mode 100644 index 00000000..a11987c8 Binary files /dev/null and b/QuickLocation/Assets.xcassets/Mine/circle_days_pair.imageset/circle_days_pair@3x.png differ diff --git a/QuickLocation/Assets.xcassets/Mine/emergency_contact.imageset/emergency_contact@2x.png b/QuickLocation/Assets.xcassets/Mine/emergency_contact.imageset/emergency_contact@2x.png index 224ef80c..a69893b3 100644 Binary files a/QuickLocation/Assets.xcassets/Mine/emergency_contact.imageset/emergency_contact@2x.png and b/QuickLocation/Assets.xcassets/Mine/emergency_contact.imageset/emergency_contact@2x.png differ diff --git a/QuickLocation/Assets.xcassets/Mine/emergency_contact.imageset/emergency_contact@3x.png b/QuickLocation/Assets.xcassets/Mine/emergency_contact.imageset/emergency_contact@3x.png index 224ef80c..d79e510e 100644 Binary files a/QuickLocation/Assets.xcassets/Mine/emergency_contact.imageset/emergency_contact@3x.png and b/QuickLocation/Assets.xcassets/Mine/emergency_contact.imageset/emergency_contact@3x.png differ diff --git a/QuickLocation/Assets.xcassets/Mine/name_heart.imageset/Contents.json b/QuickLocation/Assets.xcassets/Mine/name_heart.imageset/Contents.json new file mode 100644 index 00000000..63202522 --- /dev/null +++ b/QuickLocation/Assets.xcassets/Mine/name_heart.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "idiom" : "universal", "scale" : "1x" }, + { "filename" : "name_heart@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "name_heart@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info" : { "author" : "xcode", "version" : 1 } +} diff --git a/QuickLocation/Assets.xcassets/Mine/name_heart.imageset/name_heart@2x.png b/QuickLocation/Assets.xcassets/Mine/name_heart.imageset/name_heart@2x.png new file mode 100644 index 00000000..fd26902a Binary files /dev/null and b/QuickLocation/Assets.xcassets/Mine/name_heart.imageset/name_heart@2x.png differ diff --git a/QuickLocation/Assets.xcassets/Mine/name_heart.imageset/name_heart@3x.png b/QuickLocation/Assets.xcassets/Mine/name_heart.imageset/name_heart@3x.png new file mode 100644 index 00000000..ffcb2834 Binary files /dev/null and b/QuickLocation/Assets.xcassets/Mine/name_heart.imageset/name_heart@3x.png differ diff --git a/QuickLocation/Assets.xcassets/Mine/quick_account.imageset/Contents.json b/QuickLocation/Assets.xcassets/Mine/quick_account.imageset/Contents.json new file mode 100644 index 00000000..6402a56a --- /dev/null +++ b/QuickLocation/Assets.xcassets/Mine/quick_account.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "idiom" : "universal", "scale" : "1x" }, + { "filename" : "icon@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "icon@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info" : { "author" : "xcode", "version" : 1 } +} diff --git a/QuickLocation/Assets.xcassets/Mine/quick_account.imageset/icon@2x.png b/QuickLocation/Assets.xcassets/Mine/quick_account.imageset/icon@2x.png new file mode 100644 index 00000000..c73646cc Binary files /dev/null and b/QuickLocation/Assets.xcassets/Mine/quick_account.imageset/icon@2x.png differ diff --git a/QuickLocation/Assets.xcassets/Mine/quick_account.imageset/icon@3x.png b/QuickLocation/Assets.xcassets/Mine/quick_account.imageset/icon@3x.png new file mode 100644 index 00000000..b8e4a63c Binary files /dev/null and b/QuickLocation/Assets.xcassets/Mine/quick_account.imageset/icon@3x.png differ diff --git a/QuickLocation/Assets.xcassets/Mine/quick_status.imageset/icon@2x.png b/QuickLocation/Assets.xcassets/Mine/quick_status.imageset/icon@2x.png index 3e4150a0..d8278642 100644 Binary files a/QuickLocation/Assets.xcassets/Mine/quick_status.imageset/icon@2x.png and b/QuickLocation/Assets.xcassets/Mine/quick_status.imageset/icon@2x.png differ diff --git a/QuickLocation/Assets.xcassets/Mine/quick_status.imageset/icon@3x.png b/QuickLocation/Assets.xcassets/Mine/quick_status.imageset/icon@3x.png index 3201386a..def54b30 100644 Binary files a/QuickLocation/Assets.xcassets/Mine/quick_status.imageset/icon@3x.png and b/QuickLocation/Assets.xcassets/Mine/quick_status.imageset/icon@3x.png differ diff --git a/QuickLocation/Assets.xcassets/PigeonMessage/copy.imageset/Contents.json b/QuickLocation/Assets.xcassets/PigeonMessage/copy.imageset/Contents.json new file mode 100644 index 00000000..6c1ebb7a --- /dev/null +++ b/QuickLocation/Assets.xcassets/PigeonMessage/copy.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "idiom" : "universal", "scale" : "1x" }, + { "filename" : "copy@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "copy@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info" : { "author" : "xcode", "version" : 1 } +} diff --git a/QuickLocation/Assets.xcassets/PigeonMessage/copy.imageset/copy@2x.png b/QuickLocation/Assets.xcassets/PigeonMessage/copy.imageset/copy@2x.png new file mode 100644 index 00000000..2261aea9 Binary files /dev/null and b/QuickLocation/Assets.xcassets/PigeonMessage/copy.imageset/copy@2x.png differ diff --git a/QuickLocation/Assets.xcassets/PigeonMessage/copy.imageset/copy@3x.png b/QuickLocation/Assets.xcassets/PigeonMessage/copy.imageset/copy@3x.png new file mode 100644 index 00000000..5df8f716 Binary files /dev/null and b/QuickLocation/Assets.xcassets/PigeonMessage/copy.imageset/copy@3x.png differ diff --git a/QuickLocation/Assets.xcassets/PigeonMessage/mic_tab.imageset/Contents.json b/QuickLocation/Assets.xcassets/PigeonMessage/mic_tab.imageset/Contents.json new file mode 100644 index 00000000..046c79fe --- /dev/null +++ b/QuickLocation/Assets.xcassets/PigeonMessage/mic_tab.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "idiom" : "universal", "scale" : "1x" }, + { "filename" : "mic_tab@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "mic_tab@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info" : { "author" : "xcode", "version" : 1 } +} diff --git a/QuickLocation/Assets.xcassets/PigeonMessage/mic_tab.imageset/mic_tab@2x.png b/QuickLocation/Assets.xcassets/PigeonMessage/mic_tab.imageset/mic_tab@2x.png new file mode 100644 index 00000000..27e607f1 Binary files /dev/null and b/QuickLocation/Assets.xcassets/PigeonMessage/mic_tab.imageset/mic_tab@2x.png differ diff --git a/QuickLocation/Assets.xcassets/PigeonMessage/mic_tab.imageset/mic_tab@3x.png b/QuickLocation/Assets.xcassets/PigeonMessage/mic_tab.imageset/mic_tab@3x.png new file mode 100644 index 00000000..8a5e88f7 Binary files /dev/null and b/QuickLocation/Assets.xcassets/PigeonMessage/mic_tab.imageset/mic_tab@3x.png differ diff --git a/QuickLocation/Assets.xcassets/PigeonMessage/record_btn.imageset/Contents.json b/QuickLocation/Assets.xcassets/PigeonMessage/record_btn.imageset/Contents.json new file mode 100644 index 00000000..e768780f --- /dev/null +++ b/QuickLocation/Assets.xcassets/PigeonMessage/record_btn.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "idiom" : "universal", "scale" : "1x" }, + { "filename" : "record_btn@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "record_btn@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info" : { "author" : "xcode", "version" : 1 } +} diff --git a/QuickLocation/Assets.xcassets/PigeonMessage/record_btn.imageset/record_btn@2x.png b/QuickLocation/Assets.xcassets/PigeonMessage/record_btn.imageset/record_btn@2x.png new file mode 100644 index 00000000..ea0693a1 Binary files /dev/null and b/QuickLocation/Assets.xcassets/PigeonMessage/record_btn.imageset/record_btn@2x.png differ diff --git a/QuickLocation/Assets.xcassets/PigeonMessage/record_btn.imageset/record_btn@3x.png b/QuickLocation/Assets.xcassets/PigeonMessage/record_btn.imageset/record_btn@3x.png new file mode 100644 index 00000000..8efb2bdd Binary files /dev/null and b/QuickLocation/Assets.xcassets/PigeonMessage/record_btn.imageset/record_btn@3x.png differ diff --git a/QuickLocation/Assets.xcassets/PigeonMessage/template_apology.imageset/Contents.json b/QuickLocation/Assets.xcassets/PigeonMessage/template_apology.imageset/Contents.json new file mode 100644 index 00000000..e07f534d --- /dev/null +++ b/QuickLocation/Assets.xcassets/PigeonMessage/template_apology.imageset/Contents.json @@ -0,0 +1 @@ +{"images":[{"idiom":"universal","scale":"1x"},{"filename":"template_apology@2x.png","idiom":"universal","scale":"2x"},{"idiom":"universal","scale":"3x"}],"info":{"author":"xcode","version":1}} diff --git a/QuickLocation/Assets.xcassets/PigeonMessage/template_apology.imageset/template_apology@2x.png b/QuickLocation/Assets.xcassets/PigeonMessage/template_apology.imageset/template_apology@2x.png new file mode 100644 index 00000000..eb0f17bb Binary files /dev/null and b/QuickLocation/Assets.xcassets/PigeonMessage/template_apology.imageset/template_apology@2x.png differ diff --git a/QuickLocation/Assets.xcassets/PigeonMessage/template_apology_cover.imageset/Contents.json b/QuickLocation/Assets.xcassets/PigeonMessage/template_apology_cover.imageset/Contents.json new file mode 100644 index 00000000..de98c53c --- /dev/null +++ b/QuickLocation/Assets.xcassets/PigeonMessage/template_apology_cover.imageset/Contents.json @@ -0,0 +1 @@ +{"images":[{"idiom":"universal","scale":"1x"},{"filename":"template_apology_cover@2x.png","idiom":"universal","scale":"2x"},{"idiom":"universal","scale":"3x"}],"info":{"author":"xcode","version":1}} diff --git a/QuickLocation/Assets.xcassets/PigeonMessage/template_apology_cover.imageset/template_apology_cover@2x.png b/QuickLocation/Assets.xcassets/PigeonMessage/template_apology_cover.imageset/template_apology_cover@2x.png new file mode 100644 index 00000000..42413c38 Binary files /dev/null and b/QuickLocation/Assets.xcassets/PigeonMessage/template_apology_cover.imageset/template_apology_cover@2x.png differ diff --git a/QuickLocation/Assets.xcassets/PigeonMessage/template_arriving_soon.imageset/Contents.json b/QuickLocation/Assets.xcassets/PigeonMessage/template_arriving_soon.imageset/Contents.json new file mode 100644 index 00000000..87d64e1e --- /dev/null +++ b/QuickLocation/Assets.xcassets/PigeonMessage/template_arriving_soon.imageset/Contents.json @@ -0,0 +1 @@ +{"images":[{"idiom":"universal","scale":"1x"},{"filename":"template_arriving_soon@2x.png","idiom":"universal","scale":"2x"},{"idiom":"universal","scale":"3x"}],"info":{"author":"xcode","version":1}} diff --git a/QuickLocation/Assets.xcassets/PigeonMessage/template_arriving_soon.imageset/template_arriving_soon@2x.png b/QuickLocation/Assets.xcassets/PigeonMessage/template_arriving_soon.imageset/template_arriving_soon@2x.png new file mode 100644 index 00000000..24e18fe7 Binary files /dev/null and b/QuickLocation/Assets.xcassets/PigeonMessage/template_arriving_soon.imageset/template_arriving_soon@2x.png differ diff --git a/QuickLocation/Assets.xcassets/PigeonMessage/template_arriving_soon_cover.imageset/Contents.json b/QuickLocation/Assets.xcassets/PigeonMessage/template_arriving_soon_cover.imageset/Contents.json new file mode 100644 index 00000000..fa38b6b1 --- /dev/null +++ b/QuickLocation/Assets.xcassets/PigeonMessage/template_arriving_soon_cover.imageset/Contents.json @@ -0,0 +1 @@ +{"images":[{"idiom":"universal","scale":"1x"},{"filename":"template_arriving_soon_cover@2x.png","idiom":"universal","scale":"2x"},{"idiom":"universal","scale":"3x"}],"info":{"author":"xcode","version":1}} diff --git a/QuickLocation/Assets.xcassets/PigeonMessage/template_arriving_soon_cover.imageset/template_arriving_soon_cover@2x.png b/QuickLocation/Assets.xcassets/PigeonMessage/template_arriving_soon_cover.imageset/template_arriving_soon_cover@2x.png new file mode 100644 index 00000000..d2646f40 Binary files /dev/null and b/QuickLocation/Assets.xcassets/PigeonMessage/template_arriving_soon_cover.imageset/template_arriving_soon_cover@2x.png differ diff --git a/QuickLocation/Assets.xcassets/PigeonMessage/template_buy_for_me.imageset/Contents.json b/QuickLocation/Assets.xcassets/PigeonMessage/template_buy_for_me.imageset/Contents.json new file mode 100644 index 00000000..0e31c020 --- /dev/null +++ b/QuickLocation/Assets.xcassets/PigeonMessage/template_buy_for_me.imageset/Contents.json @@ -0,0 +1 @@ +{"images":[{"idiom":"universal","scale":"1x"},{"filename":"template_buy_for_me@2x.png","idiom":"universal","scale":"2x"},{"idiom":"universal","scale":"3x"}],"info":{"author":"xcode","version":1}} diff --git a/QuickLocation/Assets.xcassets/PigeonMessage/template_buy_for_me.imageset/template_buy_for_me@2x.png b/QuickLocation/Assets.xcassets/PigeonMessage/template_buy_for_me.imageset/template_buy_for_me@2x.png new file mode 100644 index 00000000..c7d2ac3d Binary files /dev/null and b/QuickLocation/Assets.xcassets/PigeonMessage/template_buy_for_me.imageset/template_buy_for_me@2x.png differ diff --git a/QuickLocation/Assets.xcassets/PigeonMessage/template_buy_for_me_cover.imageset/Contents.json b/QuickLocation/Assets.xcassets/PigeonMessage/template_buy_for_me_cover.imageset/Contents.json new file mode 100644 index 00000000..2553d148 --- /dev/null +++ b/QuickLocation/Assets.xcassets/PigeonMessage/template_buy_for_me_cover.imageset/Contents.json @@ -0,0 +1 @@ +{"images":[{"idiom":"universal","scale":"1x"},{"filename":"template_buy_for_me_cover@2x.png","idiom":"universal","scale":"2x"},{"idiom":"universal","scale":"3x"}],"info":{"author":"xcode","version":1}} diff --git a/QuickLocation/Assets.xcassets/PigeonMessage/template_buy_for_me_cover.imageset/template_buy_for_me_cover@2x.png b/QuickLocation/Assets.xcassets/PigeonMessage/template_buy_for_me_cover.imageset/template_buy_for_me_cover@2x.png new file mode 100644 index 00000000..0d28a5a7 Binary files /dev/null and b/QuickLocation/Assets.xcassets/PigeonMessage/template_buy_for_me_cover.imageset/template_buy_for_me_cover@2x.png differ diff --git a/QuickLocation/Assets.xcassets/PigeonMessage/template_come_play.imageset/Contents.json b/QuickLocation/Assets.xcassets/PigeonMessage/template_come_play.imageset/Contents.json new file mode 100644 index 00000000..1455a417 --- /dev/null +++ b/QuickLocation/Assets.xcassets/PigeonMessage/template_come_play.imageset/Contents.json @@ -0,0 +1 @@ +{"images":[{"idiom":"universal","scale":"1x"},{"filename":"template_come_play@2x.png","idiom":"universal","scale":"2x"},{"idiom":"universal","scale":"3x"}],"info":{"author":"xcode","version":1}} diff --git a/QuickLocation/Assets.xcassets/PigeonMessage/template_come_play.imageset/template_come_play@2x.png b/QuickLocation/Assets.xcassets/PigeonMessage/template_come_play.imageset/template_come_play@2x.png new file mode 100644 index 00000000..bf2c3108 Binary files /dev/null and b/QuickLocation/Assets.xcassets/PigeonMessage/template_come_play.imageset/template_come_play@2x.png differ diff --git a/QuickLocation/Assets.xcassets/PigeonMessage/template_come_play_cover.imageset/Contents.json b/QuickLocation/Assets.xcassets/PigeonMessage/template_come_play_cover.imageset/Contents.json new file mode 100644 index 00000000..a6c3d3bf --- /dev/null +++ b/QuickLocation/Assets.xcassets/PigeonMessage/template_come_play_cover.imageset/Contents.json @@ -0,0 +1 @@ +{"images":[{"idiom":"universal","scale":"1x"},{"filename":"template_come_play_cover@2x.png","idiom":"universal","scale":"2x"},{"idiom":"universal","scale":"3x"}],"info":{"author":"xcode","version":1}} diff --git a/QuickLocation/Assets.xcassets/PigeonMessage/template_come_play_cover.imageset/template_come_play_cover@2x.png b/QuickLocation/Assets.xcassets/PigeonMessage/template_come_play_cover.imageset/template_come_play_cover@2x.png new file mode 100644 index 00000000..aedccf8e Binary files /dev/null and b/QuickLocation/Assets.xcassets/PigeonMessage/template_come_play_cover.imageset/template_come_play_cover@2x.png differ diff --git a/QuickLocation/Assets.xcassets/PigeonMessage/template_dislike_you.imageset/Contents.json b/QuickLocation/Assets.xcassets/PigeonMessage/template_dislike_you.imageset/Contents.json new file mode 100644 index 00000000..f5c5658d --- /dev/null +++ b/QuickLocation/Assets.xcassets/PigeonMessage/template_dislike_you.imageset/Contents.json @@ -0,0 +1 @@ +{"images":[{"idiom":"universal","scale":"1x"},{"filename":"template_dislike_you@2x.png","idiom":"universal","scale":"2x"},{"idiom":"universal","scale":"3x"}],"info":{"author":"xcode","version":1}} diff --git a/QuickLocation/Assets.xcassets/PigeonMessage/template_dislike_you.imageset/template_dislike_you@2x.png b/QuickLocation/Assets.xcassets/PigeonMessage/template_dislike_you.imageset/template_dislike_you@2x.png new file mode 100644 index 00000000..1d262cdf Binary files /dev/null and b/QuickLocation/Assets.xcassets/PigeonMessage/template_dislike_you.imageset/template_dislike_you@2x.png differ diff --git a/QuickLocation/Assets.xcassets/PigeonMessage/template_dislike_you_cover.imageset/Contents.json b/QuickLocation/Assets.xcassets/PigeonMessage/template_dislike_you_cover.imageset/Contents.json new file mode 100644 index 00000000..a50290af --- /dev/null +++ b/QuickLocation/Assets.xcassets/PigeonMessage/template_dislike_you_cover.imageset/Contents.json @@ -0,0 +1 @@ +{"images":[{"idiom":"universal","scale":"1x"},{"filename":"template_dislike_you_cover@2x.png","idiom":"universal","scale":"2x"},{"idiom":"universal","scale":"3x"}],"info":{"author":"xcode","version":1}} diff --git a/QuickLocation/Assets.xcassets/PigeonMessage/template_dislike_you_cover.imageset/template_dislike_you_cover@2x.png b/QuickLocation/Assets.xcassets/PigeonMessage/template_dislike_you_cover.imageset/template_dislike_you_cover@2x.png new file mode 100644 index 00000000..32b42fcf Binary files /dev/null and b/QuickLocation/Assets.xcassets/PigeonMessage/template_dislike_you_cover.imageset/template_dislike_you_cover@2x.png differ diff --git a/QuickLocation/Assets.xcassets/PigeonMessage/template_miss_you.imageset/Contents.json b/QuickLocation/Assets.xcassets/PigeonMessage/template_miss_you.imageset/Contents.json new file mode 100644 index 00000000..0ab4128b --- /dev/null +++ b/QuickLocation/Assets.xcassets/PigeonMessage/template_miss_you.imageset/Contents.json @@ -0,0 +1 @@ +{"images":[{"idiom":"universal","scale":"1x"},{"filename":"template_miss_you@2x.png","idiom":"universal","scale":"2x"},{"idiom":"universal","scale":"3x"}],"info":{"author":"xcode","version":1}} diff --git a/QuickLocation/Assets.xcassets/PigeonMessage/template_miss_you.imageset/template_miss_you@2x.png b/QuickLocation/Assets.xcassets/PigeonMessage/template_miss_you.imageset/template_miss_you@2x.png new file mode 100644 index 00000000..37f8b9c8 Binary files /dev/null and b/QuickLocation/Assets.xcassets/PigeonMessage/template_miss_you.imageset/template_miss_you@2x.png differ diff --git a/QuickLocation/Assets.xcassets/PigeonMessage/template_miss_you_cover.imageset/Contents.json b/QuickLocation/Assets.xcassets/PigeonMessage/template_miss_you_cover.imageset/Contents.json new file mode 100644 index 00000000..c0e97305 --- /dev/null +++ b/QuickLocation/Assets.xcassets/PigeonMessage/template_miss_you_cover.imageset/Contents.json @@ -0,0 +1 @@ +{"images":[{"idiom":"universal","scale":"1x"},{"filename":"template_miss_you_cover@2x.png","idiom":"universal","scale":"2x"},{"idiom":"universal","scale":"3x"}],"info":{"author":"xcode","version":1}} diff --git a/QuickLocation/Assets.xcassets/PigeonMessage/template_miss_you_cover.imageset/template_miss_you_cover@2x.png b/QuickLocation/Assets.xcassets/PigeonMessage/template_miss_you_cover.imageset/template_miss_you_cover@2x.png new file mode 100644 index 00000000..65bc2e1d Binary files /dev/null and b/QuickLocation/Assets.xcassets/PigeonMessage/template_miss_you_cover.imageset/template_miss_you_cover@2x.png differ diff --git a/QuickLocation/Assets.xcassets/PigeonMessage/template_proud.imageset/Contents.json b/QuickLocation/Assets.xcassets/PigeonMessage/template_proud.imageset/Contents.json new file mode 100644 index 00000000..79101eb8 --- /dev/null +++ b/QuickLocation/Assets.xcassets/PigeonMessage/template_proud.imageset/Contents.json @@ -0,0 +1 @@ +{"images":[{"idiom":"universal","scale":"1x"},{"filename":"template_proud@2x.png","idiom":"universal","scale":"2x"},{"idiom":"universal","scale":"3x"}],"info":{"author":"xcode","version":1}} diff --git a/QuickLocation/Assets.xcassets/PigeonMessage/template_proud.imageset/template_proud@2x.png b/QuickLocation/Assets.xcassets/PigeonMessage/template_proud.imageset/template_proud@2x.png new file mode 100644 index 00000000..eae8971c Binary files /dev/null and b/QuickLocation/Assets.xcassets/PigeonMessage/template_proud.imageset/template_proud@2x.png differ diff --git a/QuickLocation/Assets.xcassets/PigeonMessage/template_proud_cover.imageset/Contents.json b/QuickLocation/Assets.xcassets/PigeonMessage/template_proud_cover.imageset/Contents.json new file mode 100644 index 00000000..5122f2a1 --- /dev/null +++ b/QuickLocation/Assets.xcassets/PigeonMessage/template_proud_cover.imageset/Contents.json @@ -0,0 +1 @@ +{"images":[{"idiom":"universal","scale":"1x"},{"filename":"template_proud_cover@2x.png","idiom":"universal","scale":"2x"},{"idiom":"universal","scale":"3x"}],"info":{"author":"xcode","version":1}} diff --git a/QuickLocation/Assets.xcassets/PigeonMessage/template_proud_cover.imageset/template_proud_cover@2x.png b/QuickLocation/Assets.xcassets/PigeonMessage/template_proud_cover.imageset/template_proud_cover@2x.png new file mode 100644 index 00000000..a1b73967 Binary files /dev/null and b/QuickLocation/Assets.xcassets/PigeonMessage/template_proud_cover.imageset/template_proud_cover@2x.png differ diff --git a/QuickLocation/Assets.xcassets/PigeonMessage/template_who_upset_me.imageset/Contents.json b/QuickLocation/Assets.xcassets/PigeonMessage/template_who_upset_me.imageset/Contents.json new file mode 100644 index 00000000..d0912d48 --- /dev/null +++ b/QuickLocation/Assets.xcassets/PigeonMessage/template_who_upset_me.imageset/Contents.json @@ -0,0 +1 @@ +{"images":[{"idiom":"universal","scale":"1x"},{"filename":"template_who_upset_me@2x.png","idiom":"universal","scale":"2x"},{"idiom":"universal","scale":"3x"}],"info":{"author":"xcode","version":1}} diff --git a/QuickLocation/Assets.xcassets/PigeonMessage/template_who_upset_me.imageset/template_who_upset_me@2x.png b/QuickLocation/Assets.xcassets/PigeonMessage/template_who_upset_me.imageset/template_who_upset_me@2x.png new file mode 100644 index 00000000..fb2b5b0d Binary files /dev/null and b/QuickLocation/Assets.xcassets/PigeonMessage/template_who_upset_me.imageset/template_who_upset_me@2x.png differ diff --git a/QuickLocation/Assets.xcassets/PigeonMessage/template_who_upset_me_cover.imageset/Contents.json b/QuickLocation/Assets.xcassets/PigeonMessage/template_who_upset_me_cover.imageset/Contents.json new file mode 100644 index 00000000..3d534a4e --- /dev/null +++ b/QuickLocation/Assets.xcassets/PigeonMessage/template_who_upset_me_cover.imageset/Contents.json @@ -0,0 +1 @@ +{"images":[{"idiom":"universal","scale":"1x"},{"filename":"template_who_upset_me_cover@2x.png","idiom":"universal","scale":"2x"},{"idiom":"universal","scale":"3x"}],"info":{"author":"xcode","version":1}} diff --git a/QuickLocation/Assets.xcassets/PigeonMessage/template_who_upset_me_cover.imageset/template_who_upset_me_cover@2x.png b/QuickLocation/Assets.xcassets/PigeonMessage/template_who_upset_me_cover.imageset/template_who_upset_me_cover@2x.png new file mode 100644 index 00000000..f2039fb7 Binary files /dev/null and b/QuickLocation/Assets.xcassets/PigeonMessage/template_who_upset_me_cover.imageset/template_who_upset_me_cover@2x.png differ diff --git a/QuickLocation/Assets.xcassets/PigeonMessage/voice_chip_mic.imageset/Contents.json b/QuickLocation/Assets.xcassets/PigeonMessage/voice_chip_mic.imageset/Contents.json new file mode 100644 index 00000000..6ac3762f --- /dev/null +++ b/QuickLocation/Assets.xcassets/PigeonMessage/voice_chip_mic.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "idiom" : "universal", "scale" : "1x" }, + { "filename" : "voice_chip_mic@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "voice_chip_mic@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info" : { "author" : "xcode", "version" : 1 } +} diff --git a/QuickLocation/Assets.xcassets/PigeonMessage/voice_chip_mic.imageset/voice_chip_mic@2x.png b/QuickLocation/Assets.xcassets/PigeonMessage/voice_chip_mic.imageset/voice_chip_mic@2x.png new file mode 100644 index 00000000..14bc0d12 Binary files /dev/null and b/QuickLocation/Assets.xcassets/PigeonMessage/voice_chip_mic.imageset/voice_chip_mic@2x.png differ diff --git a/QuickLocation/Assets.xcassets/PigeonMessage/voice_chip_mic.imageset/voice_chip_mic@3x.png b/QuickLocation/Assets.xcassets/PigeonMessage/voice_chip_mic.imageset/voice_chip_mic@3x.png new file mode 100644 index 00000000..1d1a80f0 Binary files /dev/null and b/QuickLocation/Assets.xcassets/PigeonMessage/voice_chip_mic.imageset/voice_chip_mic@3x.png differ diff --git a/QuickLocation/Assets.xcassets/PigeonMessage/voice_chip_play.imageset/Contents.json b/QuickLocation/Assets.xcassets/PigeonMessage/voice_chip_play.imageset/Contents.json new file mode 100644 index 00000000..c84462a5 --- /dev/null +++ b/QuickLocation/Assets.xcassets/PigeonMessage/voice_chip_play.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "idiom" : "universal", "scale" : "1x" }, + { "filename" : "voice_chip_play@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "voice_chip_play@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info" : { "author" : "xcode", "version" : 1 } +} diff --git a/QuickLocation/Assets.xcassets/PigeonMessage/voice_chip_play.imageset/voice_chip_play@2x.png b/QuickLocation/Assets.xcassets/PigeonMessage/voice_chip_play.imageset/voice_chip_play@2x.png new file mode 100644 index 00000000..eadd26b0 Binary files /dev/null and b/QuickLocation/Assets.xcassets/PigeonMessage/voice_chip_play.imageset/voice_chip_play@2x.png differ diff --git a/QuickLocation/Assets.xcassets/PigeonMessage/voice_chip_play.imageset/voice_chip_play@3x.png b/QuickLocation/Assets.xcassets/PigeonMessage/voice_chip_play.imageset/voice_chip_play@3x.png new file mode 100644 index 00000000..a4445519 Binary files /dev/null and b/QuickLocation/Assets.xcassets/PigeonMessage/voice_chip_play.imageset/voice_chip_play@3x.png differ diff --git a/QuickLocation/Assets.xcassets/SearchLocation/search_mascot.imageset/Contents.json b/QuickLocation/Assets.xcassets/SearchLocation/search_mascot.imageset/Contents.json new file mode 100644 index 00000000..6009f73c --- /dev/null +++ b/QuickLocation/Assets.xcassets/SearchLocation/search_mascot.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "idiom" : "universal", "scale" : "1x" }, + { "filename" : "search_mascot@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "search_mascot@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info" : { "author" : "xcode", "version" : 1 } +} diff --git a/QuickLocation/Assets.xcassets/SearchLocation/search_mascot.imageset/search_mascot@2x.png b/QuickLocation/Assets.xcassets/SearchLocation/search_mascot.imageset/search_mascot@2x.png new file mode 100644 index 00000000..fbcc16c6 Binary files /dev/null and b/QuickLocation/Assets.xcassets/SearchLocation/search_mascot.imageset/search_mascot@2x.png differ diff --git a/QuickLocation/Assets.xcassets/SearchLocation/search_mascot.imageset/search_mascot@3x.png b/QuickLocation/Assets.xcassets/SearchLocation/search_mascot.imageset/search_mascot@3x.png new file mode 100644 index 00000000..b21dfac5 Binary files /dev/null and b/QuickLocation/Assets.xcassets/SearchLocation/search_mascot.imageset/search_mascot@3x.png differ diff --git a/QuickLocation/Assets.xcassets/SearchLocation/search_progress_card.imageset/Contents.json b/QuickLocation/Assets.xcassets/SearchLocation/search_progress_card.imageset/Contents.json new file mode 100644 index 00000000..e21343a8 --- /dev/null +++ b/QuickLocation/Assets.xcassets/SearchLocation/search_progress_card.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "idiom" : "universal", "scale" : "1x" }, + { "filename" : "search_progress_card@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "search_progress_card@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info" : { "author" : "xcode", "version" : 1 } +} diff --git a/QuickLocation/Assets.xcassets/SearchLocation/search_progress_card.imageset/search_progress_card@2x.png b/QuickLocation/Assets.xcassets/SearchLocation/search_progress_card.imageset/search_progress_card@2x.png new file mode 100644 index 00000000..fec355fd Binary files /dev/null and b/QuickLocation/Assets.xcassets/SearchLocation/search_progress_card.imageset/search_progress_card@2x.png differ diff --git a/QuickLocation/Assets.xcassets/SearchLocation/search_progress_card.imageset/search_progress_card@3x.png b/QuickLocation/Assets.xcassets/SearchLocation/search_progress_card.imageset/search_progress_card@3x.png new file mode 100644 index 00000000..0db60eb8 Binary files /dev/null and b/QuickLocation/Assets.xcassets/SearchLocation/search_progress_card.imageset/search_progress_card@3x.png differ diff --git a/QuickLocation/Common/Constant.swift b/QuickLocation/Common/Constant.swift index 77691eea..205724c2 100644 --- a/QuickLocation/Common/Constant.swift +++ b/QuickLocation/Common/Constant.swift @@ -83,15 +83,22 @@ public let kScreenWidth = UIScreen.main.bounds.size.width public let kScreenHeight = UIScreen.main.bounds.size.height /// 状态栏高度 public var kStatusBarHeight: CGFloat { - let statusBarHeight = kKeyWindow? + if let height = kKeyWindow? .windowScene? .statusBarManager? .statusBarFrame - .height ?? 0 - return statusBarHeight + .height, + height > 1 { + return height + } + let safeTop = kKeyWindow?.safeAreaInsets.top ?? 0 + if safeTop > 1 { + return safeTop + } + return kScreenHeight >= 812 ? 47 : 20 } /// 顶部导航+状态栏高度 -public let kNaviHeight = 44 + kStatusBarHeight +public var kNaviHeight: CGFloat { 44 + kStatusBarHeight } /// App显示名称 public let kAppName = Bundle.main.infoDictionary!["CFBundleDisplayName"] as! String //App 名称 /// App的Bundle版本 diff --git a/QuickLocation/Manager/Account/Account.swift b/QuickLocation/Manager/Account/Account.swift index a47e8d3e..f0ebe0f0 100644 --- a/QuickLocation/Manager/Account/Account.swift +++ b/QuickLocation/Manager/Account/Account.swift @@ -17,6 +17,7 @@ extension DefaultsKeys { var locationHistory: DefaultsKey<[String: [[String: Any]]]?> { .init("locationHistory") } var searchShopHistory: DefaultsKey<[String]> { .init("searchShopHistory", defaultValue: []) } var searchOrderHistory: DefaultsKey<[String]> { .init("searchOrderHistory", defaultValue: []) } + var userRelations: DefaultsKey { .init("userRelations") } } struct Account: Mappable { diff --git a/QuickLocation/Manager/Account/RelationStore.swift b/QuickLocation/Manager/Account/RelationStore.swift new file mode 100644 index 00000000..af98e974 --- /dev/null +++ b/QuickLocation/Manager/Account/RelationStore.swift @@ -0,0 +1,122 @@ +// +// RelationStore.swift +// QuickLocation +// + +import Foundation +import ObjectMapper +import RxSwift +import SwiftyUserDefaults + +struct RelationListResponse: BaseModelProtocol { + var code: String? + var message: String? + var list: [RelationModel] = [] + + init?(map: Map) {} + + mutating func mapping(map: Map) { + code <- (map["code"], kIntTransformStr) + message <- map["message"] + if message == nil { + message <- map["msg"] + } + list <- map["data"] + } +} + +struct RelationModel: Mappable, Equatable { + var name: String = "" + var type: String = "" + var limit: Int = -1 + var icon: String = "" + + init?(map: Map) {} + + mutating func mapping(map: Map) { + name <- map["name"] + type <- (map["type"], kRelationTypeTransform) + limit <- (map["limit"], kStrTransformInt) + icon <- map["icon"] + } + + var idx: String { type.trimmingCharacters(in: .whitespacesAndNewlines) } + var showsHeart: Bool { idx == "1" } +} + +private let kRelationTypeTransform = TransformOf(fromJSON: { value in + transformStr(value) +}, toJSON: { value in + value +}) + +private enum RelationStoreError: LocalizedError { + case business(String) + case empty + + var errorDescription: String? { + switch self { + case let .business(message): + return message + case .empty: + return "关系列表为空" + } + } +} + +final class RelationStore { + static let shared = RelationStore() + + private(set) var list: [RelationModel] = [] + private let disposeBag = DisposeBag() + + private init() { + loadDisk() + } + + func preload(completion: ((Result<[RelationModel], Error>) -> Void)? = nil) { + UserService.relations() + .subscribe(onNext: { [weak self] response in + guard response.code == "0" else { + completion?(.failure(RelationStoreError.business(response.message ?? "获取关系列表失败"))) + return + } + guard !response.list.isEmpty else { + completion?(.failure(RelationStoreError.empty)) + return + } + self?.replace(response.list) + completion?(.success(response.list)) + }, onError: { error in + completion?(.failure(error)) + }) + .disposed(by: disposeBag) + } + + func item(for idx: String) -> RelationModel? { + let key = idx.trimmingCharacters(in: .whitespacesAndNewlines) + guard !key.isEmpty else { return nil } + return list.first { $0.idx == key } + } + + func name(for idx: String) -> String? { + let name = item(for: idx)?.name.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" + return name.isEmpty ? nil : name + } + + func showsHeart(for idx: String) -> Bool { + item(for: idx)?.showsHeart == true + } + + private func replace(_ list: [RelationModel]) { + self.list = list + let json = list.map { $0.toJSON() } + Defaults[\.userRelations] = try? JSONSerialization.data(withJSONObject: json) + } + + private func loadDisk() { + guard let data = Defaults[\.userRelations], + let json = try? JSONSerialization.jsonObject(with: data) as? [[String: Any]] else { return } + list = json.compactMap { RelationModel(JSON: $0) } + } +} diff --git a/QuickLocation/Manager/Account/UserConfigResponse.swift b/QuickLocation/Manager/Account/UserConfigResponse.swift index 28b2a335..017e95ec 100644 --- a/QuickLocation/Manager/Account/UserConfigResponse.swift +++ b/QuickLocation/Manager/Account/UserConfigResponse.swift @@ -85,7 +85,10 @@ struct UserStatusModel: Mappable { mutating func mapping(map: Map) { status <- (map["status"], kStrTransformInt) mood <- (map["mood"], kStrTransformInt) - bubbleTime <- (map["bubble_time"], kUserStatusTransformInt64) + bubbleTime <- (map["end_time"], kUserStatusTransformInt64) + if bubbleTime == 0 { + bubbleTime <- (map["bubble_time"], kUserStatusTransformInt64) + } } var bubbleEndDate: Date? { diff --git a/QuickLocation/Manager/URL/URLManager.swift b/QuickLocation/Manager/URL/URLManager.swift index 3ce1f64d..19991569 100644 --- a/QuickLocation/Manager/URL/URLManager.swift +++ b/QuickLocation/Manager/URL/URLManager.swift @@ -86,7 +86,7 @@ extension DefaultsKeys { case -1: // UAT return "https://jsapi.zuom8.cn/" default: // SIT - return "http://172.16.10.23:9243/" + return "http://172.16.10.22:9243/" } } diff --git a/QuickLocation/Model/GroupModel.swift b/QuickLocation/Model/GroupModel.swift index 48a22732..c0ae5930 100644 --- a/QuickLocation/Model/GroupModel.swift +++ b/QuickLocation/Model/GroupModel.swift @@ -364,6 +364,8 @@ struct GroupMemberModel: Mappable, Equatable { var last_position: String = "" /// 当前用户与该成员共同加入的圈子 var common_groups: [CommonGroupModel] = [] + /// 关系 extra.relation_idx + var extra = MemberExtra() var lastLocation: String { guard last_position.contains(":") else { return "" } let arr = last_position.components(separatedBy: ":") @@ -390,9 +392,66 @@ struct GroupMemberModel: Mappable, Equatable { join_time <- (map["join_time"], kGroupTransformInt64) common_groups <- map["common_groups"] common_groups = CommonGroupModel.deduplicated(common_groups) + extra <- (map["extra"], kMemberExtraTransform) + if extra.relation_idx.isEmpty { + extra.relation_idx <- (map["relation_idx"], kMemberRelationIndexTransform) + } } } +struct MemberExtra: Mappable, Equatable { + var relation_idx: String = "" + + init() {} + init?(map: Map) {} + + mutating func mapping(map: Map) { + relation_idx <- (map["relation_idx"], kMemberRelationIndexTransform) + } + + static func decode(_ value: Any?) -> MemberExtra { + if let extra = value as? MemberExtra { + return extra + } + if let json = value as? [String: Any] { + return MemberExtra(JSON: json) ?? MemberExtra() + } + if let json = value as? NSDictionary, + let dictionary = json as? [String: Any] { + return MemberExtra(JSON: dictionary) ?? MemberExtra() + } + + let data: Data? + if let value = value as? String { + data = value.data(using: .utf8) + } else { + data = value as? Data + } + guard let data, + let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { + return MemberExtra() + } + return MemberExtra(JSON: json) ?? MemberExtra() + } + + static func relationIndex(from value: Any?) -> String { + let raw = transformStr(value) ?? "" + return raw.trimmingCharacters(in: .whitespacesAndNewlines) + } +} + +let kMemberRelationIndexTransform = TransformOf(fromJSON: { value in + MemberExtra.relationIndex(from: value) +}, toJSON: { value in + value +}) + +let kMemberExtraTransform = TransformOf(fromJSON: { value in + MemberExtra.decode(value) +}, toJSON: { value in + value?.toJSON() +}) + extension GroupMemberModel { /// New responses use seconds; older payloads may still contain milliseconds. var joinDate: Date? { diff --git a/QuickLocation/Model/PigeonModel.swift b/QuickLocation/Model/PigeonModel.swift index c5d15ca7..7b320b10 100644 --- a/QuickLocation/Model/PigeonModel.swift +++ b/QuickLocation/Model/PigeonModel.swift @@ -6,6 +6,175 @@ import ObjectMapper import UIKit +struct PigeonLocalTemplate: Equatable { + let id: String + let title: String + let mediaType: String + let coverAssetName: String + let contentAssetName: String + + static let imageTemplates: [PigeonLocalTemplate] = [ + PigeonLocalTemplate( + id: "come_play", + title: "出来玩", + mediaType: "image", + coverAssetName: "PigeonMessage/template_come_play_cover", + contentAssetName: "PigeonMessage/template_come_play" + ), + PigeonLocalTemplate( + id: "miss_you", + title: "想你了", + mediaType: "image", + coverAssetName: "PigeonMessage/template_miss_you_cover", + contentAssetName: "PigeonMessage/template_miss_you" + ), + PigeonLocalTemplate( + id: "buy_for_me", + title: "给我买", + mediaType: "image", + coverAssetName: "PigeonMessage/template_buy_for_me_cover", + contentAssetName: "PigeonMessage/template_buy_for_me" + ), + PigeonLocalTemplate( + id: "dislike_you", + title: "讨厌你", + mediaType: "image", + coverAssetName: "PigeonMessage/template_dislike_you_cover", + contentAssetName: "PigeonMessage/template_dislike_you" + ), + PigeonLocalTemplate( + id: "who_upset_me", + title: "谁惹我", + mediaType: "image", + coverAssetName: "PigeonMessage/template_who_upset_me_cover", + contentAssetName: "PigeonMessage/template_who_upset_me" + ), + PigeonLocalTemplate( + id: "apology", + title: "道歉", + mediaType: "image", + coverAssetName: "PigeonMessage/template_apology_cover", + contentAssetName: "PigeonMessage/template_apology" + ), + PigeonLocalTemplate( + id: "arriving_soon", + title: "马上到", + mediaType: "image", + coverAssetName: "PigeonMessage/template_arriving_soon_cover", + contentAssetName: "PigeonMessage/template_arriving_soon" + ), + PigeonLocalTemplate( + id: "proud", + title: "骄傲", + mediaType: "image", + coverAssetName: "PigeonMessage/template_proud_cover", + contentAssetName: "PigeonMessage/template_proud" + ) + ] + + var coverImage: UIImage? { + UIImage(named: coverAssetName) + } + + var contentImage: UIImage? { + UIImage(named: contentAssetName) + } + + static func resolve(templateId: String) -> PigeonLocalTemplate? { + imageTemplates.first { $0.id == templateId } + } +} + +struct PigeonVoiceTemplate: Equatable { + let id: String + let title: String + let duration: TimeInterval + + static let templates: [PigeonVoiceTemplate] = [ + PigeonVoiceTemplate(id: "bai_bai_hao_ha", title: "表白好哈", duration: 10), + PigeonVoiceTemplate(id: "bai_bai_hao_ha_2", title: "表白好哈", duration: 10), + PigeonVoiceTemplate(id: "bai_bai_hao_ha_3", title: "表白好哈", duration: 10) + ] + + static func resolve(templateId: String) -> PigeonVoiceTemplate? { + templates.first { $0.id == templateId } + } +} + +struct PigeonTemplateExtra: Mappable { + var media_type: String = "" + var template_id: String = "" + + init?(map: Map) {} + + static func decode(_ value: Any?) -> PigeonTemplateExtra? { + if let extra = value as? PigeonTemplateExtra { + return extra + } + if let json = value as? [String: Any] { + return PigeonTemplateExtra(JSON: json) + } + + let data: Data? + if let value = value as? String { + data = value.data(using: .utf8) + } else { + data = value as? Data + } + guard let data, + let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { + return nil + } + return PigeonTemplateExtra(JSON: json) + } + + mutating func mapping(map: Map) { + media_type <- map["media_type"] + template_id <- map["template_id"] + + // Read legacy payloads, but always send the canonical two-field shape. + if template_id.isEmpty { + var legacyImageID = "" + legacyImageID <- map["image_template_id"] + if !legacyImageID.isEmpty { + template_id = legacyImageID + media_type = "image" + } else { + var legacyVoiceID = "" + legacyVoiceID <- map["voice_template_id"] + if !legacyVoiceID.isEmpty { + template_id = legacyVoiceID + media_type = "voice" + } + } + } else if media_type.isEmpty, + PigeonLocalTemplate.resolve(templateId: template_id) != nil { + media_type = "image" + } + } + + var normalizedMediaType: String { + media_type.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() + } + + var normalizedTemplateID: String { + template_id.trimmingCharacters(in: .whitespacesAndNewlines) + } + + var sendParameters: [String: Any]? { + let mediaType = normalizedMediaType + let templateID = normalizedTemplateID + guard !mediaType.isEmpty, !templateID.isEmpty else { return nil } + return ["media_type": mediaType, "template_id": templateID] + } +} + +private let kPigeonTemplateExtraTransform = TransformOf(fromJSON: { value in + PigeonTemplateExtra.decode(value) +}, toJSON: { value in + value?.sendParameters +}) + struct PigeonSentResponse: BaseModelProtocol { var code: String? var message: String? @@ -28,6 +197,8 @@ struct PigeonSentMessage: Mappable { var msg_type: Int = 0 var msg: String = "" var text_msg: String = "" + var bg_img: String = "" + var extra: PigeonTemplateExtra? var expire_time: Int64 = 0 var send_time: Int64 = 0 @@ -41,13 +212,38 @@ struct PigeonSentMessage: Mappable { msg_type <- map["msg_type"] msg <- map["msg"] text_msg <- map["text_msg"] + bg_img <- map["bg_img"] + extra <- (map["extra"], kPigeonTemplateExtraTransform) expire_time <- map["expire_time"] send_time <- map["send_time"] } + var isVoice: Bool { msg_type == 2 } + + var localTemplate: PigeonLocalTemplate? { + guard extra?.normalizedMediaType == "image" else { return nil } + let templateId = extra?.normalizedTemplateID ?? "" + return PigeonLocalTemplate.resolve(templateId: templateId) + } + + var localTemplateImage: UIImage? { + localTemplate?.contentImage + } + + var captionText: String { + if isVoice { return "" } + let fromMsg = msg.trimmingCharacters(in: .whitespacesAndNewlines) + if !fromMsg.isEmpty, !fromMsg.lowercased().hasPrefix("http") { + return fromMsg + } + return text_msg + } + var mediaURL: URL? { - let value = msg.trimmingCharacters(in: .whitespacesAndNewlines) - guard value.lowercased().hasPrefix("http"), let url = URL(string: value) else { + let value = (isVoice ? msg : bg_img).trimmingCharacters(in: .whitespacesAndNewlines) + let fallback = msg.trimmingCharacters(in: .whitespacesAndNewlines) + let raw = value.lowercased().hasPrefix("http") ? value : fallback + guard raw.lowercased().hasPrefix("http"), let url = URL(string: raw) else { return nil } return url @@ -58,9 +254,9 @@ struct PigeonSentMessage: Mappable { id: message_uuid.isEmpty ? UUID().uuidString : message_uuid, dateText: Self.dateText(from: send_time), timeText: Self.timeText(from: send_time), - kind: msg_type == 2 ? .voice : .image, - caption: text_msg, - image: nil, + kind: isVoice ? .voice : .image, + caption: captionText, + image: localTemplateImage, mediaURL: mediaURL, senderAvatar: from_user?.avatarImage, receiverAvatars: to_user.map(\.avatarImage), @@ -98,6 +294,7 @@ struct PigeonSentUser: Mappable { var nick_name: String = "" var remark: String = "" var head_pic: String = "" + var extra = MemberExtra() init?(map: Map) {} @@ -106,6 +303,10 @@ struct PigeonSentUser: Mappable { nick_name <- map["nick_name"] remark <- map["remark"] head_pic <- map["head_pic"] + extra <- (map["extra"], kMemberExtraTransform) + if extra.relation_idx.isEmpty { + extra.relation_idx <- (map["relation_idx"], kMemberRelationIndexTransform) + } } var displayName: String { diff --git a/QuickLocation/Section/Group/GroupView.swift b/QuickLocation/Section/Group/GroupView.swift index 0e10cf98..2b6e9842 100644 --- a/QuickLocation/Section/Group/GroupView.swift +++ b/QuickLocation/Section/Group/GroupView.swift @@ -9,7 +9,6 @@ import UIKit import RxSwift import RxCocoa import SDCycleScrollView -import OpenIMSDK import SwiftDate // MARK: - PanScrollView(参考 PanScrollView,允许嵌套手势同时识别) @@ -668,6 +667,8 @@ final class CircleGroupCell: UITableViewCell { label.font = .systemFont(ofSize: 12, weight: .medium) label.textColor = UIColor(hexStr: "#AAAAAA") label.lineBreakMode = .byTruncatingTail + label.setContentHuggingPriority(.defaultLow, for: .horizontal) + label.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) return label }() @@ -701,7 +702,9 @@ final class CircleGroupCell: UITableViewCell { let view = UIStackView() view.axis = .horizontal view.alignment = .center + view.distribution = .fill view.spacing = 4 + view.setContentHuggingPriority(.required, for: .horizontal) view.setContentCompressionResistancePriority(.required, for: .horizontal) return view }() @@ -765,10 +768,9 @@ final class CircleGroupCell: UITableViewCell { } func configure(_ model: GroupCellData) { - let g = model.groupInfo + let g = model.group iconView.setGroupIcon(url: model.iconURL, fallback: model.localIcon) - let count = g.memberCount - nameLabel.text = "\(g.groupName ?? "") (\(count))" + nameLabel.text = "\(g.name) (\(g.people_no))" previewLabel.text = model.lastMsgPreview if model.unreadCount > 0 { @@ -805,6 +807,8 @@ final class CircleGroupCell: UITableViewCell { label.cornerRadius = 4 label.clipsToBounds = true label.textAlignment = .center + label.setContentHuggingPriority(.required, for: .horizontal) + label.setContentCompressionResistancePriority(.required, for: .horizontal) return label } } diff --git a/QuickLocation/Section/Group/GroupViewController.swift b/QuickLocation/Section/Group/GroupViewController.swift index d200dbf4..94616ade 100644 --- a/QuickLocation/Section/Group/GroupViewController.swift +++ b/QuickLocation/Section/Group/GroupViewController.swift @@ -8,7 +8,6 @@ import RxSwift import RxCocoa import RxDataSources import SDCycleScrollView -import OpenIMSDK import ObjectMapper final class GroupViewController: BaseViewController { @@ -43,54 +42,42 @@ final class GroupViewController: BaseViewController { // MARK: - OpenIM private var hasSetupIMListeners = false - /// 进入页面加载 IM 数据:确保登录后并行拉取群列表 + 会话 + /// 进入页面加载 IM 会话:圈子集合已由业务接口给出,这里只补未读/最后一条 private func loadIMData() { GroupIMService.shared.ensureLogin { [weak self] success in guard let self = self else { return } - guard success else { - return - } + guard success else { return } self.setupIMListenersIfNeeded() self.refreshIMData() } } - /// 注册 IM 监听(仅一次):会话变化更新未读/时间;群组变化(SDK 同步到新群/退群)刷新列表 + /// 注册 IM 监听(仅一次):会话变化更新未读/时间;群组变化后重拉会话 private func setupIMListenersIfNeeded() { guard !hasSetupIMListeners else { return } hasSetupIMListeners = true - // 监听器回调已包含完整的会话列表,直接使用,避免重复 MJExtension 解析 GroupIMService.shared.setConversationListener { [weak self] conversations in - self?.viewModel.updateConversations(conversations) + DispatchQueue.main.async { + self?.viewModel.updateConversations(conversations) + } } - // 群组同步完成(如刚创建/加入的群)后刷新群列表,解决业务接口先返回、SDK 后同步的时序问题 GroupIMService.shared.setGroupListener { [weak self] in - GroupIMService.shared.getJoinedGroups { groups in - self?.viewModel.loadIMGroups(groups) + GroupIMService.shared.getConversationList { conversations in + DispatchQueue.main.async { + self?.viewModel.loadConversations(conversations) + } } } } private func refreshIMData() { - let group = DispatchGroup() - var groups: [OIMGroupInfo] = [] - var conversations: [OIMConversationInfo] = [] - - group.enter() - GroupIMService.shared.getJoinedGroups { g in - groups = g - group.leave() - } - group.enter() - GroupIMService.shared.getConversationList { c in - conversations = c - group.leave() - } - group.notify(queue: .main) { [weak self] in - DLToast.dismiss() - self?.viewModel.loadIMGroups(groups, conversations: conversations) + GroupIMService.shared.getConversationList { [weak self] conversations in + DispatchQueue.main.async { + DLToast.dismiss() + self?.viewModel.loadConversations(conversations) + } } } @@ -158,7 +145,8 @@ final class GroupViewController: BaseViewController { rootView.joinedTableView.rx.modelSelected(GroupCellData.self).asObservable() ) .subscribe(onNext: { data in - guard let groupId = data.groupInfo.groupID else { return } + let groupId = data.group.group_key + guard !groupId.isEmpty else { return } AppRouter.push(Route.groupChat, userInfo: ["groupId": groupId]) }) .disposed(by: disposeBag) @@ -319,10 +307,8 @@ final class GroupViewController: BaseViewController { private func requestGroupInfo() { GroupService.groupInfo().subscribe { response in guard let model = response.model else { return } - self.viewModel.groupList = model.groups - DispatchQueue.global(qos: .default).async { [weak self] in - self?.loadIMData() - } + self.viewModel.applyBusinessGroups(model.groups) + self.loadIMData() }.disposed(by: disposeBag) } } diff --git a/QuickLocation/Section/Group/GroupViewModel.swift b/QuickLocation/Section/Group/GroupViewModel.swift index ca191ac6..dc7b86b5 100644 --- a/QuickLocation/Section/Group/GroupViewModel.swift +++ b/QuickLocation/Section/Group/GroupViewModel.swift @@ -13,15 +13,16 @@ import ObjectMapper typealias HotGroupListSectionModel = SectionModel typealias CircleListSectionModel = SectionModel -/// 群聊 cell 数据(融合群信息 + 会话信息) +/// 群聊 cell 数据:圈子集合以业务 `groups` 为准,IM 会话只补未读/预览/时间 struct GroupCellData { - let groupInfo: OIMGroupInfo + let group: GroupInfoModel let unreadCount: Int let lastMsgTime: Int // ms timestamp let lastMsgPreview: String - let tags: [String] - let iconURL: String - let localIcon: UIImage + + var tags: [String] { Array(group.labels.prefix(2)) } + var iconURL: String { group.groupIconURL } + var localIcon: UIImage { group.groupIcon } } final class GroupViewModel { @@ -41,15 +42,8 @@ final class GroupViewModel { let segmentTitles = BehaviorRelay<[String]>(value: ["我创建的圈子", "我加入的圈子"]) - var groupList: [GroupInfoModel] = [] - - private var cachedGroups: [OIMGroupInfo] = [] + private(set) var groupList: [GroupInfoModel] = [] private var cachedConversations: [OIMConversationInfo] = [] - - /// 当前群的本地图标 - func getLocalIcon(groupId: String) -> UIImage { - groupList.first { groupId == $0.group_key }?.groupIcon ?? UIImage(named: "GroupIcon/1")! - } /// 热门酷圈 func loadHotGroupData(_ list: [GroupInfoModel]) { @@ -63,10 +57,15 @@ final class GroupViewModel { } }(self) - /// 加载群列表(全量刷新) - func loadIMGroups(_ groups: [OIMGroupInfo], conversations: [OIMConversationInfo] = []) { - if !groups.isEmpty { cachedGroups = groups } - if !conversations.isEmpty { cachedConversations = conversations } + /// 业务圈子列表(与首页切圈弹层同一份 `groups`) + func applyBusinessGroups(_ list: [GroupInfoModel]) { + groupList = list + reloadData() + } + + /// 全量刷新 IM 会话(未读、最后一条、时间) + func loadConversations(_ conversations: [OIMConversationInfo]) { + cachedConversations = conversations reloadData() } @@ -80,7 +79,6 @@ final class GroupViewModel { } private func reloadData() { - let currentUserId = AppContextManager.shared.userId let convMap: [String: OIMConversationInfo] = { var m: [String: OIMConversationInfo] = [:] for c in cachedConversations where c.groupID != nil && !(c.groupID?.isEmpty ?? true) { @@ -89,28 +87,28 @@ final class GroupViewModel { return m }() - // 「全部圈子」= 全部已加入群;「我的圈子」= 我创建的 - var all: [GroupCellData] = [] - var created: [GroupCellData] = [] - - for g in cachedGroups { - let conv = convMap[g.groupID ?? ""] - let businessGroup = groupList.first { $0.group_key == g.groupID } - let data = GroupCellData( - groupInfo: g, + func cellData(for group: GroupInfoModel) -> GroupCellData { + let conv = convMap[group.group_key] + return GroupCellData( + group: group, unreadCount: Int(conv?.unreadCount ?? 0), lastMsgTime: Int(conv?.latestMsgSendTime ?? 0), - lastMsgPreview: Self.previewText(from: conv?.latestMsg), - tags: Array((businessGroup?.labels ?? []).prefix(2)), - iconURL: businessGroup?.groupIconURL ?? "", - localIcon: getLocalIcon(groupId: g.groupID ?? "") + lastMsgPreview: Self.previewText(from: conv?.latestMsg) ) - all.append(data) - if g.ownerUserID == currentUserId || g.creatorUserID == currentUserId { - created.append(data) + } + + func sortedByLastMsg(_ items: [GroupCellData]) -> [GroupCellData] { + items.sorted { lhs, rhs in + if lhs.lastMsgTime != rhs.lastMsgTime { + return lhs.lastMsgTime > rhs.lastMsgTime + } + return lhs.group.group_key < rhs.group.group_key } } + let all = sortedByLastMsg(groupList.map(cellData(for:))) + let created = sortedByLastMsg(groupList.filter(\.is_owner).map(cellData(for:))) + createdSectionsSubject.onNext(created.mapSection()) joinedSectionsSubject.onNext(all.mapSection()) } diff --git a/QuickLocation/Section/Group/MemberInfo/GenderSelectPopVC.swift b/QuickLocation/Section/Group/MemberInfo/GenderSelectPopVC.swift new file mode 100644 index 00000000..2eb75bdd --- /dev/null +++ b/QuickLocation/Section/Group/MemberInfo/GenderSelectPopVC.swift @@ -0,0 +1,178 @@ +// +// GenderSelectPopVC.swift +// QuickLocation +// + +import UIKit + +final class GenderSelectPopVC: DLCustomPopVC { + + var onConfirm: ((Int) -> Void)? + + private var selectedSex: Int + private let options: [(sex: Int, title: String, on: String, off: String)] = [ + (1, "男", "MemberInfo/gender_male_on", "MemberInfo/gender_male_off"), + (2, "女", "MemberInfo/gender_female_on", "MemberInfo/gender_female_off"), + (0, "保密", "MemberInfo/gender_secret_on", "MemberInfo/gender_secret_off") + ] + + private var optionButtons: [UIButton] = [] + + init(currentSex: Int, onConfirm: ((Int) -> Void)? = nil) { + self.selectedSex = currentSex + self.onConfirm = onConfirm + super.init(nibName: nil, bundle: nil) + popStyle = .bottom + bottomCornerRadius = 40 + dimmingClick = true + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + setupUI() + refreshSelection() + } + + private func setupUI() { + let headerBgImage = UIImageView() + headerBgImage.image = UIImage(named: "Common/popup_header_bg") + headerBgImage.contentMode = .scaleToFill + contentView.addSubview(headerBgImage) + headerBgImage.isUserInteractionEnabled = false + headerBgImage.layoutChain + .top() + .edgesHorzontal() + .heightToWidth(120 / 375) + + let titleLab = UILabel() + titleLab.text = "性别" + titleLab.font = .systemFont(ofSize: 18, weight: .semibold) + titleLab.textColor = UIColor(hexStr: "#293445") + contentView.addSubview(titleLab) + titleLab.layoutChain.top(22).centerX() + + let closeBtn = UIButton(type: .custom) + closeBtn.setImage(UIImage(named: "MemberInfo/close_circle"), for: .normal) + closeBtn.addTarget(self, action: #selector(close), for: .touchUpInside) + contentView.addSubview(closeBtn) + closeBtn.layoutChain.right(16).centerY(titleLab).width(28).height(28) + + let row = UIStackView() + row.axis = .horizontal + row.alignment = .fill + row.distribution = .fillEqually + row.spacing = 16 + contentView.addSubview(row) + row.layoutChain + .topToBottomOfView(titleLab, offset: 24) + .left(28) + .right(28) + .height(100) + + for (index, option) in options.enumerated() { + let item = makeOption(option, tag: index) + optionButtons.append(item.button) + row.addArrangedSubview(item.wrap) + } + contentView.bringSubviewToFront(row) + + let confirm = UIButton(type: .custom) + confirm.setTitle("确 定", for: .normal) + confirm.setTitleColor(.white, for: .normal) + confirm.titleLabel?.font = FontManager.boboBold(18) + confirm.setBackgroundImage(UIImage(named: "Common/button_bg_2"), for: .normal) + confirm.layer.cornerRadius = 20 + confirm.clipsToBounds = true + confirm.addTarget(self, action: #selector(confirmTapped), for: .touchUpInside) + contentView.addSubview(confirm) + confirm.layoutChain + .topToBottomOfView(row, offset: 22) + .left(30) + .right(30) + .height(50) + .bottom(16 + kSafeBottomMargin) + + let height = 22 + 28 + 24 + 100 + 22 + 50 + 16 + kSafeBottomMargin + contentBgView.layoutChain.height(height) + contentView.layoutChain.height(height) + view.layoutIfNeeded() + } + + private func makeOption( + _ option: (sex: Int, title: String, on: String, off: String), + tag: Int + ) -> (wrap: UIView, button: UIButton) { + let wrap = UIControl() + wrap.tag = tag + wrap.addTarget(self, action: #selector(tapOption(_:)), for: .touchUpInside) + let button = UIButton(type: .custom) + button.isUserInteractionEnabled = false + button.tag = tag + button.backgroundColor = UIColor(hexStr: "#F5F6F8") + button.layer.cornerRadius = 16 + button.clipsToBounds = true + wrap.addSubview(button) + button.layoutChain.top().centerX().width(72).height(72) + + let lab = UILabel() + lab.text = option.title + lab.font = .systemFont(ofSize: 14, weight: .medium) + lab.textColor = UIColor(hexStr: "#293445") + lab.textAlignment = .center + wrap.addSubview(lab) + lab.layoutChain.topToBottomOfView(button, offset: 8).centerX() + return (wrap, button) + } + + private func refreshSelection() { + for (index, button) in optionButtons.enumerated() { + let option = options[index] + let selected = selectedSex == option.sex + button.layer.borderWidth = selected ? 1.5 : 0 + button.layer.borderColor = UIColor(hexStr: "#16B3FF").cgColor + let name = selected ? option.on : option.off + button.setImage(UIImage(named: name), for: .normal) + } + } + + @objc private func tapOption(_ sender: UIControl) { + guard options.indices.contains(sender.tag) else { return } + selectedSex = options[sender.tag].sex + refreshSelection() + } + + @objc private func confirmTapped() { + let sex = selectedSex + dismiss(animated: true) { [weak self] in + self?.onConfirm?(sex) + } + } + + @objc private func close() { + dismiss(animated: true) + } +} + +private final class GenderPopGradientView: UIView { + override class var layerClass: AnyClass { CAGradientLayer.self } + + override init(frame: CGRect) { + super.init(frame: frame) + isUserInteractionEnabled = false + guard let gradient = layer as? CAGradientLayer else { return } + gradient.colors = [ + UIColor(hexStr: "#BAF4FF").cgColor, + UIColor(hexStr: "#BAF4FF", alpha: 0).cgColor + ] + gradient.startPoint = CGPoint(x: 0.5, y: 0) + gradient.endPoint = CGPoint(x: 0.5, y: 1) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/QuickLocation/Section/Group/MemberInfo/MemberInfoVC.swift b/QuickLocation/Section/Group/MemberInfo/MemberInfoVC.swift index feb482e5..187bf9c7 100644 --- a/QuickLocation/Section/Group/MemberInfo/MemberInfoVC.swift +++ b/QuickLocation/Section/Group/MemberInfo/MemberInfoVC.swift @@ -5,6 +5,7 @@ import UIKit import ObjectMapper +import RxSwift final class MemberInfoVC: BaseViewController { @@ -12,6 +13,7 @@ final class MemberInfoVC: BaseViewController { private var members: [GroupMemberModel] private var selectedUserId: String private var groupModel: GroupModel? + private var isLoadingRelations = false init(members: [GroupMemberModel], selectedUserId: String, groupModel: GroupModel?) { self.members = members @@ -74,19 +76,19 @@ final class MemberInfoVC: BaseViewController { switch AppContextManager.shared.sex { case 1: return "男" case 2: return "女" + case 0: return "保密" default: return "未设置" } }() rootView.renderRows([ MemberInfoRow(title: "头像", value: "", kind: .avatar, avatarURL: member?.head_pic) { [weak self] in - AppRouter.push(Route.myProfile) - _ = self + self?.editAvatar() }, - MemberInfoRow(title: "昵称", value: member?.nick_name ?? "", kind: .disclosure) { - AppRouter.push(Route.myProfile) + MemberInfoRow(title: "昵称", value: member?.nick_name ?? "", kind: .disclosure) { [weak self] in + self?.editNickname() }, - MemberInfoRow(title: "性别", value: sexText, kind: .disclosure) { - AppRouter.push(Route.myProfile) + MemberInfoRow(title: "性别", value: sexText, kind: .disclosure) { [weak self] in + self?.editGender() }, MemberInfoRow(title: "添加时间", value: joinText, kind: .text), commonGroupsRow @@ -97,8 +99,8 @@ final class MemberInfoVC: BaseViewController { MemberInfoRow(title: "备注", value: member?.remark ?? "", kind: .disclosure) { DLToast.show(text: "敬请期待") }, - MemberInfoRow(title: "关系", value: "未绑定", kind: .disclosure) { - DLToast.show(text: "敬请期待") + MemberInfoRow(title: "关系", value: relationText(for: member), kind: .disclosure) { [weak self] in + self?.editRelation() }, MemberInfoRow(title: "性别", value: "保密", kind: .text), MemberInfoRow(title: "添加时间", value: joinText, kind: .text), @@ -153,6 +155,170 @@ final class MemberInfoVC: BaseViewController { return CommonGroupModel.deduplicated(groups) } + private func editAvatar() { + let current = AppContextManager.shared.head_pic + let vc = AvatarIconListVC(iconIndex: current) + vc.onSelectIcon = { [weak self] index in + self?.requestSetHeadPic(index: index) + } + navigationController?.pushViewController(vc, animated: true) + } + + private func editNickname() { + let current = AppContextManager.shared.name + let vc = TextInputViewController( + title: "昵称", + maxLength: 10, + initialText: current + ) { [weak self] text in + self?.requestSetNickName(text) + } + present(vc, animated: true) + } + + private func relationText(for member: GroupMemberModel?) -> String { + RelationStore.shared.name(for: member?.extra.relation_idx ?? "") ?? "未绑定" + } + + private func editRelation() { + if RelationStore.shared.list.isEmpty { + guard !isLoadingRelations else { return } + isLoadingRelations = true + DLToast.showLoading() + RelationStore.shared.preload { [weak self] result in + MainAsync { + guard let self else { return } + self.isLoadingRelations = false + DLToast.dismiss() + switch result { + case .success: + self.presentRelationPicker() + case let .failure(error): + DLToast.showError(text: error.localizedDescription) + } + } + } + return + } + presentRelationPicker() + } + + private func presentRelationPicker() { + guard presentedViewController == nil else { return } + let current = members.first { $0.user_id == selectedUserId }?.extra.relation_idx ?? "" + let pop = RelationSelectPopVC(currentIdx: current) { [weak self] idx in + self?.requestEditRelation(idx) + } + present(pop, animated: true) + } + + private func requestEditRelation(_ idx: String) { + guard let member = members.first(where: { $0.user_id == selectedUserId }) else { return } + let groupKey = groupModel?.default_group_key ?? "" + guard !groupKey.isEmpty else { + DLToast.show(text: "圈子信息缺失") + return + } + let nick = member.remark.trimmingCharacters(in: .whitespacesAndNewlines) + let nickName = nick.isEmpty ? member.nick_name : nick + DLToast.showLoading() + GroupService.operate( + opType: "editmateinfo", + requestData: [ + "group_key": groupKey, + "user_id": member.user_id, + "nick_name": nickName, + "extra": ["relation_idx": idx] + ] + ).subscribe(onNext: { [weak self] _ in + DLToast.show(text: "设置成功") + guard let self, let index = self.members.firstIndex(where: { $0.user_id == self.selectedUserId }) else { return } + self.members[index].extra.relation_idx = idx + self.refreshDetail() + }, onError: { error in + DLToast.dismiss() + DLToast.show(text: error.localizedDescription) + }).disposed(by: disposeBag) + } + + private func editGender() { + let pop = GenderSelectPopVC(currentSex: AppContextManager.shared.sex) { [weak self] sex in + self?.requestSetGender(sex) + } + present(pop, animated: true) + } + + private func requestSetHeadPic(index: Int) { + DLToast.showLoading() + UserService.setHeadPic(index: index).subscribe(onNext: { [weak self] _ in + DLToast.show(text: "更换成功") + self?.applyLocalMemberChange(headPic: "\(index)", nickName: nil) + self?.syncUserConfig() + }, onError: { error in + DLToast.dismiss() + DLToast.show(text: error.localizedDescription) + }).disposed(by: disposeBag) + } + + private func requestSetNickName(_ name: String) { + let trimmed = name.trimmingCharacters(in: .whitespacesAndNewlines) + guard !trimmed.isEmpty else { return } + DLToast.showLoading() + UserService.setNickName(nick: trimmed).subscribe(onNext: { [weak self] _ in + DLToast.show(text: "修改成功") + self?.applyLocalMemberChange(headPic: nil, nickName: trimmed) + self?.syncUserConfig() + }, onError: { error in + DLToast.dismiss() + DLToast.show(text: error.localizedDescription) + }).disposed(by: disposeBag) + } + + private func requestSetGender(_ sex: Int) { + DLToast.showLoading() + UserService.setGender(sex: sex).subscribe(onNext: { [weak self] _ in + DLToast.show(text: "修改成功") + if var account = AppContextManager.shared.account { + account.sex = sex + AppContextManager.shared.saveAccount(account) + } + self?.refreshDetail() + self?.syncUserConfig() + }, onError: { error in + DLToast.dismiss() + DLToast.show(text: error.localizedDescription) + }).disposed(by: disposeBag) + } + + private func applyLocalMemberChange(headPic: String?, nickName: String?) { + if let index = members.firstIndex(where: { $0.user_id == selectedUserId }) { + if let headPic { + members[index].head_pic = headPic + } + if let nickName { + members[index].nick_name = nickName + } + } + if var account = AppContextManager.shared.account { + if let headPic { account.head_pic = headPic } + if let nickName { account.name = nickName } + AppContextManager.shared.saveAccount(account) + } + rootView.memberCV.reloadData() + refreshDetail() + } + + private func syncUserConfig() { + UserService.userInfo().subscribe(onNext: { response in + if let model = response.model { + AppContextManager.shared.saveAccount(model) + } + NotificationCenter.default.post(name: .RefreshUserConfigNotification, object: nil) + }, onError: { _ in + NotificationCenter.default.post(name: .RefreshUserConfigNotification, object: nil) + }).disposed(by: disposeBag) + } + private func showCommonGroups(_ groups: [CommonGroupModel]) { guard groups.count > 1 else { return } let popup = CommonGroupsPopVC(groups: groups, groupInfos: groupModel?.groups ?? []) diff --git a/QuickLocation/Section/Group/MemberInfo/RelationSelectPopVC.swift b/QuickLocation/Section/Group/MemberInfo/RelationSelectPopVC.swift new file mode 100644 index 00000000..490281ea --- /dev/null +++ b/QuickLocation/Section/Group/MemberInfo/RelationSelectPopVC.swift @@ -0,0 +1,168 @@ +// +// RelationSelectPopVC.swift +// QuickLocation +// + +import UIKit + +final class RelationSelectPopVC: DLCustomPopVC { + + var onConfirm: ((String) -> Void)? + + private var selectedIdx: String + private let items: [RelationModel] + private var chipButtons: [UIButton] = [] + + init(currentIdx: String, onConfirm: ((String) -> Void)? = nil) { + self.selectedIdx = currentIdx + self.items = RelationStore.shared.list + self.onConfirm = onConfirm + super.init(nibName: nil, bundle: nil) + popStyle = .bottom + bottomCornerRadius = 40 + dimmingClick = true + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + setupUI() + refreshSelection() + } + + private func setupUI() { + let headerBgImage = UIImageView() + headerBgImage.image = UIImage(named: "Common/popup_header_bg") + headerBgImage.contentMode = .scaleToFill + headerBgImage.isUserInteractionEnabled = false + contentView.addSubview(headerBgImage) + headerBgImage.layoutChain + .top() + .edgesHorzontal() + .heightToWidth(120 / 375) + + let titleLab = UILabel() + titleLab.text = "关系设置" + titleLab.font = .systemFont(ofSize: 18, weight: .semibold) + titleLab.textColor = UIColor(hexStr: "#293445") + contentView.addSubview(titleLab) + titleLab.layoutChain.top(22).centerX() + + let closeBtn = UIButton(type: .custom) + closeBtn.setImage(UIImage(named: "MemberInfo/close_circle"), for: .normal) + closeBtn.addTarget(self, action: #selector(close), for: .touchUpInside) + contentView.addSubview(closeBtn) + closeBtn.layoutChain.right(16).centerY(titleLab).width(28).height(28) + + let columns = 5 + let rows = max(1, Int(ceil(Double(max(items.count, 1)) / Double(columns)))) + let chipHeight: CGFloat = 36 + let rowSpacing: CGFloat = 8 + let gridHeight = CGFloat(rows) * chipHeight + CGFloat(max(0, rows - 1)) * rowSpacing + + let grid = UIStackView() + grid.axis = .vertical + grid.spacing = rowSpacing + grid.distribution = .fillEqually + contentView.addSubview(grid) + grid.layoutChain + .topToBottomOfView(titleLab, offset: 24) + .left(16) + .right(16) + .height(gridHeight) + + for row in 0.. UIButton { + let button = UIButton(type: .custom) + button.tag = tag + button.layer.cornerRadius = 18 + button.clipsToBounds = true + button.titleLabel?.font = .systemFont(ofSize: 13, weight: .medium) + button.addTarget(self, action: #selector(tapChip(_:)), for: .touchUpInside) + if item.showsHeart { + let heart = UIImage(named: "Mine/name_heart")?.withRenderingMode(.alwaysOriginal) + button.setImage(heart, for: .normal) + button.setTitle(" \(item.name)", for: .normal) + button.imageView?.contentMode = .scaleAspectFit + button.imageEdgeInsets = UIEdgeInsets(top: 0, left: -2, bottom: 0, right: 2) + } else { + button.setTitle(item.name, for: .normal) + } + return button + } + + private func refreshSelection() { + for (index, button) in chipButtons.enumerated() { + guard items.indices.contains(index) else { continue } + let selected = items[index].idx == selectedIdx + button.backgroundColor = selected ? UIColor(hexStr: "#16B3FF") : .white + button.setTitleColor(selected ? .white : UIColor(hexStr: "#293445"), for: .normal) + button.layer.borderWidth = selected ? 0 : 1 + button.layer.borderColor = UIColor(hexStr: "#E8EEF2").cgColor + } + } + + @objc private func tapChip(_ sender: UIButton) { + guard items.indices.contains(sender.tag) else { return } + selectedIdx = items[sender.tag].idx + refreshSelection() + } + + @objc private func confirmTapped() { + guard !selectedIdx.isEmpty else { + DLToast.show(text: "请选择关系") + return + } + let idx = selectedIdx + dismiss(animated: true) { [weak self] in + self?.onConfirm?(idx) + } + } + + @objc private func close() { + dismiss(animated: true) + } +} diff --git a/QuickLocation/Section/Home/Bubble/CreateBubbleSetupView.swift b/QuickLocation/Section/Home/Bubble/CreateBubbleSetupView.swift index c4b79c9a..3839c722 100644 --- a/QuickLocation/Section/Home/Bubble/CreateBubbleSetupView.swift +++ b/QuickLocation/Section/Home/Bubble/CreateBubbleSetupView.swift @@ -134,7 +134,7 @@ final class CreateBubbleSetupView: UIView { headerBgImage.layoutChain .top() .edgesHorzontal() - .heightToWidth(239.0 / 750.0) + .heightToWidth(120 / 375) megaphoneImage.image = UIImage(named: "Bubble/notification_megaphone") megaphoneImage.contentMode = .scaleAspectFit diff --git a/QuickLocation/Section/Home/GroupListPopView.swift b/QuickLocation/Section/Home/GroupListPopView.swift index 108b6663..609fd94d 100644 --- a/QuickLocation/Section/Home/GroupListPopView.swift +++ b/QuickLocation/Section/Home/GroupListPopView.swift @@ -310,6 +310,8 @@ class GroupListCell: UITableViewCell { label.cornerRadius = 4 label.clipsToBounds = true label.textAlignment = .center + label.setContentHuggingPriority(.required, for: .horizontal) + label.setContentCompressionResistancePriority(.required, for: .horizontal) return label } @@ -398,7 +400,9 @@ class GroupListCell: UITableViewCell { let view = UIStackView() view.axis = .horizontal view.alignment = .center + view.distribution = .fill view.spacing = 4 + view.setContentHuggingPriority(.required, for: .horizontal) view.setContentCompressionResistancePriority(.required, for: .horizontal) return view }() diff --git a/QuickLocation/Section/Home/HomeViewController.swift b/QuickLocation/Section/Home/HomeViewController.swift index b10b2e84..4b148ffc 100644 --- a/QuickLocation/Section/Home/HomeViewController.swift +++ b/QuickLocation/Section/Home/HomeViewController.swift @@ -483,16 +483,24 @@ class HomeViewController: BaseViewController { } private func makeReceiveDisplayItem(_ message: PigeonSentMessage) -> ReceiveMessageDisplayItem { - ReceiveMessageDisplayItem( + let localTemplateImage = message.localTemplateImage + let senderId = message.from_user?.user_id ?? "" + let memberRelationIdx = viewModel.memberList.first { $0.user_id == senderId }? + .extra.relation_idx.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" + let relationIdx = memberRelationIdx.isEmpty + ? message.from_user?.extra.relation_idx ?? "" + : memberRelationIdx + return ReceiveMessageDisplayItem( messageUUID: message.message_uuid, senderName: message.from_user?.displayName ?? "圈子成员", avatar: message.from_user?.avatarImage, - image: nil, - imageURL: message.msg_type == 2 ? nil : message.mediaURL, - message: message.text_msg, - kind: message.msg_type == 2 ? .voice : .image, + image: localTemplateImage, + imageURL: message.isVoice || localTemplateImage != nil ? nil : message.mediaURL, + message: message.captionText, + kind: message.isVoice ? .voice : .image, duration: 0, - audioURL: message.msg_type == 2 ? message.mediaURL : nil + audioURL: message.isVoice ? message.mediaURL : nil, + relationIdx: relationIdx ) } diff --git a/QuickLocation/Section/Home/PhoneReportDetail/PhoneReportDetailView.swift b/QuickLocation/Section/Home/PhoneReportDetail/PhoneReportDetailView.swift index 78b0dd5f..8337fbea 100644 --- a/QuickLocation/Section/Home/PhoneReportDetail/PhoneReportDetailView.swift +++ b/QuickLocation/Section/Home/PhoneReportDetail/PhoneReportDetailView.swift @@ -48,6 +48,7 @@ final class PhoneReportDetailView: UIView { let contentView = UIView() // MARK: Status + let statusTitleIcon = UIImageView(image: UIImage(named: "Home/report_phone")) let statusTitleLab = UILabel() let statusCard = UIView() let batteryRing = PhoneBatteryRingView() @@ -65,6 +66,7 @@ final class PhoneReportDetailView: UIView { // MARK: Charts let screenSection = PhoneReportChartSectionView( title: "屏幕使用时间", + iconName: "Home/report_screen", summaryPrefix: "今日使用时长", summaryColor: UIColor(hexStr: "#00ADFE"), barGradient: (UIColor(hexStr: "#C0EAFF"), UIColor(hexStr: "#ECF9FF")), @@ -72,7 +74,8 @@ final class PhoneReportDetailView: UIView { tipColor: UIColor(hexStr: "#20B6FF") ) let unlockSection = PhoneReportChartSectionView( - title: "手机使用次数", + title: "APP使用次数", + iconName: "Home/report_usage", summaryPrefix: "今日使用次数", summaryColor: UIColor(hexStr: "#68C025"), barGradient: (UIColor(hexStr: "#DDFFA1"), UIColor(hexStr: "#F2FFDC")), @@ -118,17 +121,27 @@ final class PhoneReportDetailView: UIView { .edgesHorzontal(15) .height(100) + statusTitleIcon.contentMode = .scaleAspectFit + contentView.addSubview(statusTitleIcon) + statusTitleIcon.layoutChain + .topToBottomOfView(memberCV, offset: 12) + .left(15) + .width(18) + .height(18) + statusTitleLab.text = "手机状态" statusTitleLab.font = .systemFont(ofSize: 16, weight: .bold) statusTitleLab.textColor = UIColor(hexStr: "#293445") contentView.addSubview(statusTitleLab) - statusTitleLab.layoutChain.topToBottomOfView(memberCV, offset: 12).left(15) + statusTitleLab.layoutChain + .centerY(statusTitleIcon) + .leftToRightOfView(statusTitleIcon, offset: 6) statusCard.backgroundColor = .white statusCard.applyPhoneReportCardStyle(cornerRadius: 20) contentView.addSubview(statusCard) statusCard.layoutChain - .topToBottomOfView(statusTitleLab, offset: 10) + .topToBottomOfView(statusTitleIcon, offset: 10) .edgesHorzontal(15) .height(175) @@ -253,7 +266,7 @@ final class PhoneBatteryRingView: UIView { private let track = CAShapeLayer() private let progress = CAShapeLayer() private let valueLab = UILabel() - private let bolt = UIImageView(image: UIImage(systemName: "bolt.fill")) + private let bolt = UIImageView(image: UIImage(named: "Home/report_bolt")) override init(frame: CGRect) { super.init(frame: frame) @@ -268,7 +281,6 @@ final class PhoneBatteryRingView: UIView { progress.strokeEnd = 0 layer.addSublayer(progress) - bolt.tintColor = UIColor(hexStr: "#27DB4B") bolt.contentMode = .scaleAspectFit addSubview(bolt) valueLab.font = .systemFont(ofSize: 16, weight: .heavy) @@ -276,7 +288,11 @@ final class PhoneBatteryRingView: UIView { valueLab.textAlignment = .center addSubview(valueLab) valueLab.layoutChain.centerX().centerY() - bolt.layoutChain.centerY(valueLab).rightToLeftOfView(valueLab) + bolt.layoutChain + .centerY(valueLab) + .rightToLeftOfView(valueLab, offset: -2) + .width(12) + .height(14) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -292,7 +308,7 @@ final class PhoneBatteryRingView: UIView { func configure(percent: Int?, text: String, isCharging: Bool = false) { valueLab.text = text - bolt.isHidden = !isCharging + bolt.isHidden = false progress.strokeEnd = CGFloat(max(0, min(100, percent ?? 0))) / 100 } } @@ -348,6 +364,7 @@ final class PhoneReportChartSectionView: UIView { init( title: String, + iconName: String, summaryPrefix: String, summaryColor: UIColor, barGradient: (UIColor, UIColor), @@ -362,11 +379,15 @@ final class PhoneReportChartSectionView: UIView { self.tipColor = tipColor self.valueSuffix = valueSuffix super.init(frame: .zero) + let titleIcon = UIImageView(image: UIImage(named: iconName)) + titleIcon.contentMode = .scaleAspectFit + addSubview(titleIcon) + titleIcon.layoutChain.top().left().width(18).height(18) titleLab.text = title titleLab.font = .systemFont(ofSize: 16, weight: .bold) titleLab.textColor = UIColor(hexStr: "#293445") addSubview(titleLab) - titleLab.layoutChain.top().left() + titleLab.layoutChain.centerY(titleIcon).leftToRightOfView(titleIcon, offset: 6) let card = UIView() card.backgroundColor = .white diff --git a/QuickLocation/Section/Home/ReceiveMessagePopView.swift b/QuickLocation/Section/Home/ReceiveMessagePopView.swift index 01c98148..4e9e1f30 100644 --- a/QuickLocation/Section/Home/ReceiveMessagePopView.swift +++ b/QuickLocation/Section/Home/ReceiveMessagePopView.swift @@ -22,6 +22,7 @@ struct ReceiveMessageDisplayItem { let kind: ReceiveMessageKind let duration: TimeInterval let audioURL: URL? + let relationIdx: String init( messageUUID: String = "", @@ -32,7 +33,8 @@ struct ReceiveMessageDisplayItem { message: String, kind: ReceiveMessageKind = .image, duration: TimeInterval = 0, - audioURL: URL? = nil + audioURL: URL? = nil, + relationIdx: String = "" ) { self.messageUUID = messageUUID self.senderName = senderName @@ -43,6 +45,7 @@ struct ReceiveMessageDisplayItem { self.kind = kind self.duration = duration self.audioURL = audioURL + self.relationIdx = relationIdx } } @@ -175,8 +178,8 @@ final class ReceiveMessagePopView: UIView { private let relationLabel: UILabel = { let label = UILabel() - label.text = "💕" - label.font = .systemFont(ofSize: 15) + label.font = .systemFont(ofSize: 13, weight: .medium) + label.textColor = UIColor(hexStr: "#FF6B9D") return label }() @@ -184,10 +187,10 @@ final class ReceiveMessagePopView: UIView { private let messageImageView: UIImageView = { let view = UIImageView() - view.backgroundColor = UIColor(hexStr: "#D9D9D9") + view.backgroundColor = .clear view.contentMode = .scaleAspectFill view.clipsToBounds = true - view.layer.cornerRadius = 29 + view.layer.cornerRadius = 30 view.isUserInteractionEnabled = true return view }() @@ -367,6 +370,7 @@ final class ReceiveMessagePopView: UIView { private func configure(item: ReceiveMessageDisplayItem) { stopVoicePlayback(reset: true) nameLabel.text = item.senderName.isEmpty ? "圈子成员" : item.senderName + applyRelation(item.relationIdx) avatarView.image = item.avatar ?? UIImage(named: "UserIcon/1") messageImageView.kf.cancelDownloadTask() if let imageURL = item.imageURL { @@ -387,6 +391,19 @@ final class ReceiveMessagePopView: UIView { } } + private func applyRelation(_ idx: String) { + if RelationStore.shared.showsHeart(for: idx) { + relationLabel.text = "💕" + relationLabel.isHidden = false + } else if let name = RelationStore.shared.name(for: idx) { + relationLabel.text = name + relationLabel.isHidden = false + } else { + relationLabel.text = nil + relationLabel.isHidden = true + } + } + private func animateInitialFlyIn() { burnButton.isUserInteractionEnabled = false voiceOverlay.isUserInteractionEnabled = false diff --git a/QuickLocation/Section/Home/SearchLocation/SearchLocationVC.swift b/QuickLocation/Section/Home/SearchLocation/SearchLocationVC.swift index c88e7b76..cc9fdb08 100644 --- a/QuickLocation/Section/Home/SearchLocation/SearchLocationVC.swift +++ b/QuickLocation/Section/Home/SearchLocation/SearchLocationVC.swift @@ -37,11 +37,9 @@ class SearchLocationVC: BaseViewController { } self.requestSearchPhone(phone: phone) self.rootView.searchProgressView.isHidden = false - self.rootView.searchPhoneLottieView.play() self.rootView.playVideo { self.rootView.videoView.isHidden = true self.rootView.searchProgressView.isHidden = true - self.rootView.searchPhoneLottieView.stop() guard self.code != -999 else { DLToast.show(text: "发生未知错误,请重试") diff --git a/QuickLocation/Section/Home/SearchLocation/SearchLocationView.swift b/QuickLocation/Section/Home/SearchLocation/SearchLocationView.swift index 70ba01da..2f261d39 100644 --- a/QuickLocation/Section/Home/SearchLocation/SearchLocationView.swift +++ b/QuickLocation/Section/Home/SearchLocation/SearchLocationView.swift @@ -49,9 +49,9 @@ class SearchLocationView: UIView { videoView.layoutChain.edges() searchProgressView.layoutChain - .edgesHorzontal(15) + .edgesHorzontal(30) .bottom(kSafeBottomMargin + 20) - .height(221) + .heightToWidth(184.0 / 315.0) navBgView.layoutChain .edges(excludingEdge: .bottom) @@ -260,20 +260,39 @@ class SearchLocationView: UIView { lazy var searchProgressView: UIView = { let view = UIView() - view.backgroundColor = .black.withAlphaComponent(0.8) - view.cornerRadius = 16 - view.borderWidth = 0.5 - view.borderColor = .white + view.backgroundColor = .clear view.isHidden = true + view.layer.cornerRadius = 24 + view.layer.borderWidth = 0.6 + view.layer.borderColor = UIColor.white.withAlphaComponent(0.35).cgColor + view.clipsToBounds = true - view.addSubview(searchPhoneLottieView) - searchPhoneLottieView.layoutChain - .top(30) - .left(15) - .width(120) - .heightToWidth(1) + let blur = UIVisualEffectView(effect: UIBlurEffect(style: .systemUltraThinMaterialDark)) + view.addSubview(blur) + blur.layoutChain.edges() - let steps = ["分析用户号码", "正在找 CGI 蜂窝参数", "SS7 信息交流", "号码已获授权", "处理完成"] + let card = UIImageView(image: UIImage(named: "SearchLocation/search_progress_card")) + card.contentMode = .scaleToFill + card.alpha = 0.12 + view.addSubview(card) + card.layoutChain.edges() + + let mascot = UIImageView(image: UIImage(named: "SearchLocation/search_mascot")) + mascot.contentMode = .scaleAspectFit + view.addSubview(mascot) + mascot.layoutChain + .left(16) + .top(20) + .width(100) + .height(100) + + let steps = [ + "分析用户号码…", + "正在找 CGI 蜂窝参数…", + "SS7 信息交流…", + "号码已获授权…", + "处理完成…" + ] var prevStepView: UIView? for text in steps { let stepView = UIView() @@ -286,45 +305,47 @@ class SearchLocationView: UIView { let label = UILabel() label.text = text - label.font = .systemFont(ofSize: 16, weight: .medium) + label.font = .systemFont(ofSize: 13, weight: .medium) label.textColor = .white stepView.addSubview(label) - icon.layoutChain.left().centerY().width(18).height(18) - label.layoutChain.leftToRightOfView(icon, offset: 8).centerY().right() + icon.layoutChain.left().centerY().width(16).height(16) + label.layoutChain.leftToRightOfView(icon, offset: 6).centerY().right() stepView.layoutChain - .leftToRightOfView(searchPhoneLottieView, offset: 10) - .right(10) - .height(26) + .leftToRightOfView(mascot, offset: 12) + .right(16) + .height(22) if let prev = prevStepView { - stepView.layoutChain.topToBottomOfView(prev, offset: 4) + stepView.layoutChain.topToBottomOfView(prev, offset: 2) } else { - stepView.layoutChain.top(20) + stepView.layoutChain.top(16) } prevStepView = stepView } - progressBar.layer.cornerRadius = 4 + progressBar.layer.cornerRadius = 3 progressBar.clipsToBounds = true - progressBar.trackTintColor = .white.withAlphaComponent(0.2) + progressBar.trackTintColor = UIColor.white.withAlphaComponent(0.22) progressBar.progressTintColor = UIColor(hexStr: "#16B3FF") view.addSubview(progressBar) + progressLab.text = "0%" progressLab.textColor = .white progressLab.font = .systemFont(ofSize: 12, weight: .medium) progressLab.textAlignment = .right view.addSubview(progressLab) progressBar.layoutChain - .topToBottomOfView(prevStepView!, offset: 15) + .bottom(16) .left(20) - .height(8) + .height(6) progressLab.layoutChain .leftToRightOfView(progressBar, offset: 8) .centerY(progressBar) - .right(20) + .right(16) + .width(36) return view }() @@ -355,6 +376,7 @@ class SearchLocationView: UIView { lazy var searchPhoneLottieView: LottieAnimationView = { let view = LottieAnimationView(name: "phone_search_interlude") view.loopMode = .loop + view.isHidden = true return view }() diff --git a/QuickLocation/Section/Launch/LaunchViewController.swift b/QuickLocation/Section/Launch/LaunchViewController.swift index 925ddb57..481851c3 100644 --- a/QuickLocation/Section/Launch/LaunchViewController.swift +++ b/QuickLocation/Section/Launch/LaunchViewController.swift @@ -59,6 +59,7 @@ class LaunchViewController: BaseViewController { guard let model = response.model else { return } MQTTService.shared.connect() Defaults[\.loginToken] = model.token + RelationStore.shared.preload() AppContextManager.shared.systemConfig = model.config diff --git a/QuickLocation/Section/Login/LoginViewModel.swift b/QuickLocation/Section/Login/LoginViewModel.swift index f3b8f817..8b21bf36 100644 --- a/QuickLocation/Section/Login/LoginViewModel.swift +++ b/QuickLocation/Section/Login/LoginViewModel.swift @@ -25,6 +25,7 @@ enum LoginSessionHandler { guard let model = response.model else { return } NotificationCenter.default.post(name: .invalidatePopupQueue, object: nil) Defaults[\.loginToken] = model.token + RelationStore.shared.preload() DLToast.showSuccess(text: "登录成功") { if let userId = model.uid { MQTTService.shared.updateClientID("smartdrive_\(userId)") diff --git a/QuickLocation/Section/Mine/MinePhotoWallView.swift b/QuickLocation/Section/Mine/MinePhotoWallView.swift index 0ef69bbc..9ac0c18a 100644 --- a/QuickLocation/Section/Mine/MinePhotoWallView.swift +++ b/QuickLocation/Section/Mine/MinePhotoWallView.swift @@ -21,6 +21,7 @@ final class MinePhotoWallView: UIView { private var centeredIndex: Int = 0 private var isDragging = false private var lastLayoutWidth: CGFloat = 0 + private var isPairMode = false /// cell 包络按选中相框 70×90 + 夹子留白 private let itemSize = CGSize(width: 72, height: 110) @@ -61,21 +62,43 @@ final class MinePhotoWallView: UIView { // MARK: - Public - func reload(members: [GroupMemberModel], ownerUserId: String, preferUserId: String) { - self.members = members + func reload( + members: [GroupMemberModel], + ownerUserId: String, + preferUserId: String, + memberLimit: Int = -1 + ) { + isPairMode = memberLimit == 2 + self.members = isPairMode ? Array(members.prefix(2)) : members self.ownerUserId = ownerUserId - self.clipColorIndices = Self.makeClipColorIndices(for: members) + self.clipColorIndices = Self.makeClipColorIndices(for: self.members) + collectionView.isScrollEnabled = !isPairMode + daysBadgeImg.image = UIImage(named: isPairMode ? "Mine/circle_days_pair" : "Mine/circle_days") collectionView.reloadData() + applyPairInsetsIfNeeded() - guard !members.isEmpty else { + guard !self.members.isEmpty else { updateInfo(nil) return } + if isPairMode { + centeredIndex = 0 + collectionView.setContentOffset(.zero, animated: false) + collectionView.layoutIfNeeded() + applyArcAndFocus() + DispatchQueue.main.async { [weak self] in + self?.applyArcAndFocus() + } + updateInfo(self.members[0]) + onCenteredMember?(self.members[0]) + return + } + let idx: Int - if let i = members.firstIndex(where: { $0.user_id == preferUserId }) { + if let i = self.members.firstIndex(where: { $0.user_id == preferUserId }) { idx = i - } else if let i = members.firstIndex(where: { $0.user_id == AppContextManager.shared.userId }) { + } else if let i = self.members.firstIndex(where: { $0.user_id == AppContextManager.shared.userId }) { idx = i } else { idx = 0 @@ -85,12 +108,11 @@ final class MinePhotoWallView: UIView { scrollToIndex(idx, animated: false) collectionView.layoutIfNeeded() applyArcAndFocus() - // contentOffset / cell 布局下一拍再贴一次,避免首屏右侧离绳 DispatchQueue.main.async { [weak self] in self?.applyArcAndFocus() } - updateInfo(members[idx]) - onCenteredMember?(members[idx]) + updateInfo(self.members[idx]) + onCenteredMember?(self.members[idx]) } /// 相邻夹子不同色;同成员尽量稳定 @@ -162,6 +184,31 @@ final class MinePhotoWallView: UIView { return label }() + private lazy var pairHeartImg: UIImageView = { + let iv = UIImageView(image: UIImage(named: "Mine/name_heart")) + iv.contentMode = .scaleAspectFit + iv.isHidden = true + return iv + }() + + private lazy var pairLeftLab: UILabel = { + let label = UILabel() + label.font = .systemFont(ofSize: 16, weight: .heavy) + label.textColor = UIColor(hexStr: "#293445") + label.textAlignment = .right + label.isHidden = true + return label + }() + + private lazy var pairRightLab: UILabel = { + let label = UILabel() + label.font = .systemFont(ofSize: 16, weight: .heavy) + label.textColor = UIColor(hexStr: "#293445") + label.textAlignment = .left + label.isHidden = true + return label + }() + private lazy var daysRow: UIView = { let v = UIView() v.addSubview(daysPrefixLab) @@ -235,6 +282,9 @@ final class MinePhotoWallView: UIView { layer.addSublayer(ropeLayer) addSubview(collectionView) addSubview(titleLab) + addSubview(pairHeartImg) + addSubview(pairLeftLab) + addSubview(pairRightLab) addSubview(daysRow) collectionView.layoutChain @@ -247,6 +297,20 @@ final class MinePhotoWallView: UIView { .centerX() .height(24) + pairHeartImg.layoutChain + .centerX() + .centerY(titleLab) + .width(11) + .height(9) + pairLeftLab.layoutChain + .centerY(titleLab) + .rightToLeftOfView(pairHeartImg, offset: -8) + .left(16, relation: .greaterThanOrEqual) + pairRightLab.layoutChain + .centerY(titleLab) + .leftToRightOfView(pairHeartImg, offset: 8) + .right(16, relation: .greaterThanOrEqual) + daysRow.layoutChain .topToBottomOfView(titleLab, offset: 4) .centerX() @@ -264,9 +328,7 @@ final class MinePhotoWallView: UIView { let widthChanged = abs(bounds.width - lastLayoutWidth) > 0.5 if widthChanged, let layout = collectionView.collectionViewLayout as? MinePhotoWallFlowLayout { lastLayoutWidth = bounds.width - let side = (bounds.width - itemSize.width) / 2 - let topInset = ropeCenterY - clipAttachInCell - layout.sectionInset = UIEdgeInsets(top: max(0, topInset), left: side, bottom: 0, right: side) + applyPairInsetsIfNeeded() layout.invalidateLayout() collectionView.layoutIfNeeded() } @@ -296,6 +358,20 @@ final class MinePhotoWallView: UIView { ropeShadowLayer.frame = bounds } + private func applyPairInsetsIfNeeded() { + guard let layout = collectionView.collectionViewLayout as? MinePhotoWallFlowLayout else { return } + let topInset = ropeCenterY - clipAttachInCell + let side: CGFloat + if isPairMode { + let count = CGFloat(max(members.count, 1)) + let contentW = count * itemSize.width + max(0, count - 1) * itemSpacing + side = max(0, (bounds.width - contentW) / 2) + } else { + side = (bounds.width - itemSize.width) / 2 + } + layout.sectionInset = UIEdgeInsets(top: max(0, topInset), left: side, bottom: 0, right: side) + } + private func scrollToIndex(_ index: Int, animated: Bool) { guard index >= 0, index < members.count else { return } let indexPath = IndexPath(item: index, section: 0) @@ -351,7 +427,7 @@ final class MinePhotoWallView: UIView { cell.transform = .identity let dist = abs(attrs.center.x - scrollCenterX) - let t = min(dist / span, 1) + let t = isPairMode ? 0 : min(dist / span, 1) // 相框真实尺寸插值(不含夹子),避免非等比 scale + 旋转剪切变形 let fw = selectedSize.width + (unselectedSize.width - selectedSize.width) * t let fh = selectedSize.height + (unselectedSize.height - selectedSize.height) * t @@ -372,7 +448,7 @@ final class MinePhotoWallView: UIView { polaroid.setFrameAlpha(frameAlpha) polaroid.applyFocus(frameSize: CGSize(width: fw, height: fh), angle: angle) - polaroid.setSelectedLook(dist < itemSize.width * 0.45) + polaroid.setSelectedLook(isPairMode || dist < itemSize.width * 0.45, pairMode: isPairMode) cell.transform = CGAffineTransform(translationX: 0, y: ty) } } @@ -380,12 +456,35 @@ final class MinePhotoWallView: UIView { private func updateInfo(_ model: GroupMemberModel?) { guard let model = model else { titleLab.text = " " + titleLab.isHidden = false + pairHeartImg.isHidden = true + pairLeftLab.isHidden = true + pairRightLab.isHidden = true daysRow.isHidden = true return } - let nickname = model.nick_name.trimmingCharacters(in: .whitespacesAndNewlines) - titleLab.text = nickname.isEmpty ? "未设置昵称" : nickname - daysLab.text = model.joinDaysDisplay + if isPairMode { + titleLab.isHidden = true + pairHeartImg.isHidden = members.count < 2 + pairLeftLab.isHidden = false + pairRightLab.isHidden = members.count < 2 + let names = members.map { member -> String in + let name = member.nick_name.trimmingCharacters(in: .whitespacesAndNewlines) + return name.isEmpty ? "未设置昵称" : name + } + pairLeftLab.text = names.first ?? " " + pairRightLab.text = names.count >= 2 ? names[1] : "" + let selfMember = members.first { $0.user_id == AppContextManager.shared.userId } ?? model + daysLab.text = selfMember.joinDaysDisplay + } else { + titleLab.isHidden = false + pairHeartImg.isHidden = true + pairLeftLab.isHidden = true + pairRightLab.isHidden = true + let nickname = model.nick_name.trimmingCharacters(in: .whitespacesAndNewlines) + titleLab.text = nickname.isEmpty ? "未设置昵称" : nickname + daysLab.text = model.joinDaysDisplay + } daysRow.isHidden = false } @@ -424,6 +523,10 @@ extension MinePhotoWallView: UICollectionViewDataSource, UICollectionViewDelegat func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { let model = members[indexPath.item] + if isPairMode { + onTapMember?(model) + return + } centeredIndex = indexPath.item let before = collectionView.contentOffset scrollToIndex(indexPath.item, animated: true) @@ -512,16 +615,19 @@ final class MinePolaroidCell: UICollectionViewCell { private static let swayKey = "minePolaroidSway" private var isOwner = false + private var relationIdx: String = "" private var focusFrameSize = selectedFrameSize private var focusAngle: CGFloat = 0 func configure(model: GroupMemberModel, isOwner: Bool, clipColorIndex: Int) { self.isOwner = isOwner + self.relationIdx = model.extra.relation_idx avatarImg.image = model.userIcon ownerTag.isHidden = !isOwner let idx = ((clipColorIndex % Self.clipAssetNames.count) + Self.clipAssetNames.count) % Self.clipAssetNames.count clipImg.image = UIImage(named: Self.clipAssetNames[idx]) stopSway() + applyCaption(visible: true) } /// 按真实相框尺寸布局,整块 polaroidHost 绕衣夹只做旋转(无非等比 scale) @@ -537,13 +643,38 @@ final class MinePolaroidCell: UICollectionViewCell { clipImg.alpha = 1 } - func setSelectedLook(_ selected: Bool) { - heartImg.isHidden = !selected - if selected { - ownerTag.isHidden = true - } else { - ownerTag.isHidden = !isOwner + func setSelectedLook(_ selected: Bool, pairMode: Bool = false) { + applyCaption(visible: pairMode || selected) + } + + private func applyCaption(visible: Bool) { + let boundName = RelationStore.shared.name(for: relationIdx) + let isHeart = RelationStore.shared.showsHeart(for: relationIdx) + let bound = boundName != nil + + if isOwner { + ownerTag.isHidden = false + heartImg.isHidden = !isHeart + relationLab.isHidden = true + layoutPolaroidHostIfNeeded() + return } + + ownerTag.isHidden = true + guard visible else { + heartImg.isHidden = true + relationLab.isHidden = true + return + } + if isHeart || !bound { + heartImg.isHidden = false + relationLab.isHidden = true + } else { + heartImg.isHidden = true + relationLab.isHidden = false + relationLab.text = boundName + } + layoutPolaroidHostIfNeeded() } func playSwayOnce() { @@ -567,8 +698,10 @@ final class MinePolaroidCell: UICollectionViewCell { super.prepareForReuse() stopSway() heartImg.isHidden = true + relationLab.isHidden = true ownerTag.isHidden = true isOwner = false + relationIdx = "" focusFrameSize = Self.selectedFrameSize focusAngle = 0 polaroidHost.transform = .identity @@ -618,10 +751,24 @@ final class MinePolaroidCell: UICollectionViewCell { avatarImg.cornerRadius = max(4, avatarSide * 0.2) let captionMidY = Self.avatarInset + avatarSide + (fh - Self.avatarInset - avatarSide) / 2 + layoutCaption(frameWidth: fw, midY: captionMidY) + } + + private func layoutCaption(frameWidth fw: CGFloat, midY: CGFloat) { heartImg.bounds = CGRect(x: 0, y: 0, width: 12, height: 10) - heartImg.center = CGPoint(x: fw / 2, y: captionMidY) + relationLab.sizeToFit() ownerTag.sizeToFit() - ownerTag.center = CGPoint(x: fw / 2, y: captionMidY) + var views: [UIView] = [] + if !heartImg.isHidden { views.append(heartImg) } + if !relationLab.isHidden { views.append(relationLab) } + if !ownerTag.isHidden { views.append(ownerTag) } + let spacing: CGFloat = 2 + let total = views.reduce(CGFloat(0)) { $0 + $1.bounds.width } + spacing * CGFloat(max(0, views.count - 1)) + var x = (fw - total) / 2 + for view in views { + view.center = CGPoint(x: x + view.bounds.width / 2, y: midY) + x += view.bounds.width + spacing + } } override init(frame: CGRect) { @@ -633,6 +780,7 @@ final class MinePolaroidCell: UICollectionViewCell { swayHost.addSubview(frameView) frameView.addSubview(avatarImg) frameView.addSubview(heartImg) + frameView.addSubview(relationLab) frameView.addSubview(ownerTag) swayHost.addSubview(clipImg) } @@ -690,6 +838,15 @@ final class MinePolaroidCell: UICollectionViewCell { return iv }() + private lazy var relationLab: UILabel = { + let label = UILabel() + label.font = .systemFont(ofSize: 9, weight: .semibold) + label.textColor = UIColor(hexStr: "#FF6B9D") + label.textAlignment = .center + label.isHidden = true + return label + }() + private lazy var ownerTag: UILabel = { let label = PaddingLabel() label.text = "圈主" diff --git a/QuickLocation/Section/Mine/MineView.swift b/QuickLocation/Section/Mine/MineView.swift index caf908bf..34fa60ce 100644 --- a/QuickLocation/Section/Mine/MineView.swift +++ b/QuickLocation/Section/Mine/MineView.swift @@ -259,7 +259,7 @@ final class MineView: UIView { "Mine/quick_permission", "Mine/quick_data_security", "Mine/quick_status", - "Mine/quick_status" + "Mine/quick_account" ] for (i, title) in quickItemTitles.enumerated() { let item = makeQuickItem(icon: icons[i], title: title) diff --git a/QuickLocation/Section/Mine/MineViewController.swift b/QuickLocation/Section/Mine/MineViewController.swift index eb0e33c1..419d5863 100644 --- a/QuickLocation/Section/Mine/MineViewController.swift +++ b/QuickLocation/Section/Mine/MineViewController.swift @@ -121,10 +121,14 @@ final class MineViewController: BaseViewController { let ownerId = sorted.first { GroupOwnerMatcher.matches(userId: $0.user_id, groupKey: groupKey) }?.user_id ?? "" + let limit = groupModel?.groups.first { + $0.group_key == groupKey + }?.group_template?.limit ?? -1 rootView.photoWall.reload( members: sorted, ownerUserId: ownerId, - preferUserId: AppContextManager.shared.userId + preferUserId: AppContextManager.shared.userId, + memberLimit: limit ) } diff --git a/QuickLocation/Section/Mine/MineViewModel.swift b/QuickLocation/Section/Mine/MineViewModel.swift index 5f709ec9..cb5f8ee4 100644 --- a/QuickLocation/Section/Mine/MineViewModel.swift +++ b/QuickLocation/Section/Mine/MineViewModel.swift @@ -36,7 +36,7 @@ class MineViewModel { MineMenuItem(icon: "Mine/quick_permission", title: "权限检测"), MineMenuItem(icon: "Mine/quick_data_security", title: "数据安全"), MineMenuItem(icon: "Mine/quick_status", title: "状态设置"), - MineMenuItem(icon: "Mine/quick_status", title: "账户设置") + MineMenuItem(icon: "Mine/quick_account", title: "账户设置") ] lazy var cellAction: Action = { this in diff --git a/QuickLocation/Section/PigeonMessage/PigeonMessageHistoryView.swift b/QuickLocation/Section/PigeonMessage/PigeonMessageHistoryView.swift index bc830e3b..a5b37b3b 100644 --- a/QuickLocation/Section/PigeonMessage/PigeonMessageHistoryView.swift +++ b/QuickLocation/Section/PigeonMessage/PigeonMessageHistoryView.swift @@ -211,10 +211,12 @@ final class PigeonHistoryCell: UITableViewCell { imageContainer.isHidden = isVoice voiceBar.isHidden = !isVoice photoView.kf.cancelDownloadTask() - if let mediaURL = item.mediaURL, !isVoice { + if let image = item.image, !isVoice { + photoView.image = image + } else if let mediaURL = item.mediaURL, !isVoice { photoView.kf.setImage(with: mediaURL, placeholder: nil) } else { - photoView.image = item.image + photoView.image = nil } let caption = item.caption.trimmingCharacters(in: .whitespacesAndNewlines) captionBackground.isHidden = caption.isEmpty diff --git a/QuickLocation/Section/PigeonMessage/PigeonMessageVC.swift b/QuickLocation/Section/PigeonMessage/PigeonMessageVC.swift index 5c34a4ca..9f1473e9 100644 --- a/QuickLocation/Section/PigeonMessage/PigeonMessageVC.swift +++ b/QuickLocation/Section/PigeonMessage/PigeonMessageVC.swift @@ -15,6 +15,8 @@ final class PigeonMessageVC: BaseViewController { private var members: [GroupMemberModel] = [] private var selectedMembers: [GroupMemberModel] = [] private var selectedImage: UIImage? + private var selectedTemplate: PigeonLocalTemplate? + private var selectedVoiceTemplate: PigeonVoiceTemplate? private var audioRecorder: AVAudioRecorder? private var audioPlayer: AVAudioPlayer? @@ -48,8 +50,17 @@ final class PigeonMessageVC: BaseViewController { rootView.onSelectionChanged = { [weak self] members in self?.selectedMembers = members } - rootView.addImageButton.addTarget(self, action: #selector(selectImage), for: .touchUpInside) - rootView.deleteImageButton.addTarget(self, action: #selector(deleteImage), for: .touchUpInside) + rootView.onUploadTapped = { [weak self] in + self?.selectImage() + } + rootView.onTemplateSelected = { [weak self] template in + self?.applySelectedTemplate(template) + } + rootView.onVoiceTemplateSelected = { [weak self] template in + self?.selectedVoiceTemplate = template + self?.stopPlayback() + } + rootView.deleteImageButton.addTarget(self, action: #selector(deleteCaption), for: .touchUpInside) rootView.playPauseButton.addTarget(self, action: #selector(togglePlayback), for: .touchUpInside) rootView.sendButton.addTarget(self, action: #selector(sendMessage), for: .touchUpInside) rootView.onModeChanged = { [weak self] _ in @@ -80,6 +91,7 @@ final class PigeonMessageVC: BaseViewController { } self.groupModel = model self.activeGroupKey = model.default_group_key + self.updateCurrentGroupDisplay() self.loadUsers(for: self.activeGroupKey, showLoading: false) }, onError: { error in DLToast.dismiss() @@ -115,6 +127,13 @@ final class PigeonMessageVC: BaseViewController { rootView.configureMembers(members, selectedIds: []) } + private func updateCurrentGroupDisplay() { + let groupName = groupModel?.groups.first { + $0.group_key == activeGroupKey + }?.name ?? "" + rootView.setCurrentGroupName(groupName) + } + @objc private func goBack() { navigationController?.popViewController(animated: true) } @@ -131,6 +150,7 @@ final class PigeonMessageVC: BaseViewController { ) { [weak self] groupKey in guard let self, let key = groupKey, key != self.activeGroupKey else { return } self.activeGroupKey = key + self.updateCurrentGroupDisplay() self.loadUsers(for: key, showLoading: true) } } @@ -191,10 +211,21 @@ final class PigeonMessageVC: BaseViewController { } private func applySelectedImage(_ image: UIImage) { + selectedTemplate = nil selectedImage = image rootView.setSelectedImage(image) } + private func applySelectedTemplate(_ template: PigeonLocalTemplate) { + guard let image = template.contentImage else { + DLToast.show(text: "模板资源不可用") + return + } + selectedTemplate = template + selectedImage = image + rootView.setSelectedImage(image, selectedTemplateID: template.id) + } + private func showCameraDenied() { Permission.openAppSetting( title: "无法使用相机", @@ -202,9 +233,8 @@ final class PigeonMessageVC: BaseViewController { ) } - @objc private func deleteImage() { - selectedImage = nil - rootView.setSelectedImage(nil) + @objc private func deleteCaption() { + rootView.clearCaption() } @objc private func handleRecordGesture(_ gesture: UILongPressGestureRecognizer) { @@ -405,50 +435,79 @@ final class PigeonMessageVC: BaseViewController { return } - let upload: Observable - let msgType: Int - switch rootView.mode { - case .image: - guard let image = selectedImage, - let jpeg = compressedPigeonJPEG(from: image) else { + guard selectedImage != nil else { + DLToast.show(text: "请先选择一张图片") + return + } + + let isVoice = rootView.mode == .voice + if isVoice, selectedVoiceTemplate == nil, recordedDuration < 1 { + DLToast.show(text: "请先完成录音") + return + } + + let msgType = isVoice ? 2 : 3 + let caption = rootView.captionText.trimmingCharacters(in: .whitespacesAndNewlines) + var extra: [String: Any] = [:] + if let selectedTemplate { + extra["media_type"] = selectedTemplate.mediaType + extra["template_id"] = selectedTemplate.id + } + if isVoice, let selectedVoiceTemplate { + extra["media_type"] = "voice" + extra["template_id"] = selectedVoiceTemplate.id + } + + let imageUpload: Observable + if selectedTemplate == nil { + guard let image = selectedImage, let jpeg = compressedPigeonJPEG(from: image) else { DLToast.show(text: "请先选择一张图片") return } - upload = UploadService.upload(jpeg, kind: .jpeg, scene: "pigeon") - msgType = 1 - case .voice: - guard let recordURL, recordedDuration >= 1, - let voiceData = try? Data(contentsOf: recordURL) else { + imageUpload = UploadService.upload(jpeg, kind: .jpeg, scene: "pigeon") + } else { + imageUpload = .just("") + } + + let voiceUpload: Observable + if isVoice, selectedVoiceTemplate == nil { + guard let recordURL, let voiceData = try? Data(contentsOf: recordURL) else { DLToast.show(text: "请先完成录音") return } - upload = UploadService.upload(voiceData, kind: .wav, scene: "pigeon") - msgType = 2 + voiceUpload = UploadService.upload(voiceData, kind: .wav, scene: "pigeon") + } else { + voiceUpload = .just("") } DLToast.showLoading() - upload - .flatMap { [weak self] fileId -> Observable in + Observable.zip(imageUpload, voiceUpload) + .flatMap { [weak self] bgImg, voiceId -> Observable in guard let self else { return .empty() } + let msg: String + if isVoice { + msg = voiceId + } else { + msg = caption + } return PigeonService.send( groupKey: self.activeGroupKey, toUsers: self.selectedMembers.map(\.user_id), msgType: msgType, - fileId: fileId, - textMsg: self.rootView.captionText.trimmingCharacters(in: .whitespacesAndNewlines) + msg: msg, + bgImg: bgImg, + extra: extra.isEmpty ? nil : extra ) } .subscribe(onNext: { [weak self] _ in DLToast.dismiss() guard let self else { return } - switch self.rootView.mode { - case .image: - self.selectedImage = nil - self.rootView.clearImageDraft() - case .voice: - self.cleanupAudio(deleteFile: true) - self.rootView.clearVoiceDraft() - } + self.selectedTemplate = nil + self.selectedVoiceTemplate = nil + self.selectedImage = nil + self.rootView.clearImageDraft() + self.cleanupAudio(deleteFile: true) + self.rootView.clearVoiceDraft() DLToast.show(text: "发送成功") }, onError: { error in DLToast.dismiss() diff --git a/QuickLocation/Section/PigeonMessage/PigeonMessageView.swift b/QuickLocation/Section/PigeonMessage/PigeonMessageView.swift index 12ca9e51..3970bb6d 100644 --- a/QuickLocation/Section/PigeonMessage/PigeonMessageView.swift +++ b/QuickLocation/Section/PigeonMessage/PigeonMessageView.swift @@ -17,6 +17,9 @@ final class PigeonMessageView: UIView, UITextFieldDelegate { var onModeChanged: ((PigeonMessageMode) -> Void)? var onSelectionChanged: (([GroupMemberModel]) -> Void)? + var onUploadTapped: (() -> Void)? + var onTemplateSelected: ((PigeonLocalTemplate) -> Void)? + var onVoiceTemplateSelected: ((PigeonVoiceTemplate?) -> Void)? var mode: PigeonMessageMode = .image { didSet { updateMode() @@ -30,7 +33,6 @@ final class PigeonMessageView: UIView, UITextFieldDelegate { let historyButton = UIButton(type: .custom) let switchGroupButton = UIButton(type: .custom) - let addImageButton = UIButton(type: .custom) let deleteImageButton = UIButton(type: .custom) let recordButton = UIButton(type: .custom) let playPauseButton = UIButton(type: .custom) @@ -44,7 +46,7 @@ final class PigeonMessageView: UIView, UITextFieldDelegate { layout.scrollDirection = .horizontal layout.minimumLineSpacing = 0 layout.minimumInteritemSpacing = 0 - layout.itemSize = CGSize(width: 66, height: 84) + layout.itemSize = CGSize(width: 68, height: 76) return layout }() @@ -57,6 +59,7 @@ final class PigeonMessageView: UIView, UITextFieldDelegate { view.register(PigeonMemberCell.self, forCellWithReuseIdentifier: PigeonMemberCell.reuseId) return view }() + private let recipientFadeView = PigeonRecipientFadeView() private let imageModeButton = UIButton(type: .custom) private let voiceModeButton = UIButton(type: .custom) @@ -65,19 +68,20 @@ final class PigeonMessageView: UIView, UITextFieldDelegate { private let selectedImageView = UIImageView() private let selectedImageMaskView = UIView() private let imagePlaceholderLabel = UILabel() + private let overlayCaptionBar = UIView() + private let overlayCaptionLabel = UILabel() + private let copyFooter = UIView() private let captionField = UITextField() private let captionCountLabel = UILabel() - private let imageThumbnailView = UIImageView() - private let imageThumbnailButton = UIButton(type: .custom) - private let addTile = UIView() + private let templatePickerView = PigeonTemplatePickerView() + private let voiceChipPicker = PigeonVoiceChipPickerView() + private let playbackBar = UIView() private let voiceThumbnailView = UIView() private let recordOuterHaloView = UIView() private let recordInnerHaloView = UIView() private let recordingHintLabel = UILabel() private let voiceTimeLabel = UILabel() private let waveformView = PigeonWaveformView() - private var addTileLeadingWithImage: NSLayoutConstraint? - private var addTileLeadingWithoutImage: NSLayoutConstraint? private var selectedImageHeightConstraint: NSLayoutConstraint? private var isRecording = false @@ -111,18 +115,43 @@ final class PigeonMessageView: UIView, UITextFieldDelegate { self.members = members self.selectedMemberIds = selectedIds memberCollectionView.reloadData() + setNeedsLayout() } - func setSelectedImage(_ image: UIImage?) { + func setCurrentGroupName(_ name: String) { + let trimmedName = name.trimmingCharacters(in: .whitespacesAndNewlines) + var titleAttributes = AttributeContainer() + titleAttributes.font = .systemFont(ofSize: scaled(12), weight: .medium) + + var configuration = switchGroupButton.configuration ?? UIButton.Configuration.plain() + configuration.attributedTitle = AttributedString( + trimmedName.isEmpty ? "当前圈子" : trimmedName, + attributes: titleAttributes + ) + configuration.image = UIImage(named: "Home/group_name_icon") + configuration.imagePadding = scaled(3) + configuration.contentInsets = NSDirectionalEdgeInsets( + top: 0, + leading: scaled(6), + bottom: 0, + trailing: scaled(6) + ) + configuration.baseForegroundColor = UIColor(hexStr: "#293445") + switchGroupButton.configuration = configuration + switchGroupButton.titleLabel?.lineBreakMode = .byTruncatingTail + switchGroupButton.sizeToFit() + } + + func setSelectedImage(_ image: UIImage?, selectedTemplateID: String? = nil) { selectedImageView.image = image selectedImageView.isHidden = image == nil selectedImageMaskView.isHidden = image == nil imagePlaceholderLabel.isHidden = image != nil - imageThumbnailView.image = image - imageThumbnailButton.isHidden = image == nil // deleteImageButton.isHidden = image == nil - addTile.alpha = 1 - updateAddTilePosition(hasImage: image != nil) + templatePickerView.setSelection( + templateID: selectedTemplateID, + customImageSelected: image != nil && selectedTemplateID == nil + ) setNeedsLayout() } @@ -164,6 +193,7 @@ final class PigeonMessageView: UIView, UITextFieldDelegate { if !hasRecording, !isRecording { waveformView.reset() } + updatePreviewOverlays() } func setPlaying(_ isPlaying: Bool) { @@ -186,9 +216,14 @@ final class PigeonMessageView: UIView, UITextFieldDelegate { waveformView.push(level: level) } - func clearImageDraft() { + func clearCaption() { captionField.text = nil captionCountLabel.text = "0/20" + updatePreviewOverlays() + } + + func clearImageDraft() { + clearCaption() setSelectedImage(nil) } @@ -199,11 +234,9 @@ final class PigeonMessageView: UIView, UITextFieldDelegate { } private func setupUI() { - let headerView = UIImageView(image: UIImage(named: "PigeonMessage/bg")) - headerView.contentMode = .scaleToFill - headerView.clipsToBounds = true + let headerView = PigeonHeaderGradientView() addSubview(headerView) - headerView.layoutChain.top().edgesHorzontal().height(scaled(210)) + headerView.layoutChain.top().edgesHorzontal().heightToWidth(135/375) historyButton.backgroundColor = .white.withAlphaComponent(0.8) historyButton.setTitle(" 传书记录 ", for: .normal) @@ -228,19 +261,19 @@ final class PigeonMessageView: UIView, UITextFieldDelegate { .height(56) let scrollView = UIScrollView() - scrollView.backgroundColor = UIColor(hexStr: "#F9F9F9") + scrollView.backgroundColor = .clear scrollView.layer.cornerRadius = scaled(30) scrollView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] // scrollView.alwaysBounceVertical = true scrollView.showsVerticalScrollIndicator = false addSubview(scrollView) scrollView.layoutChain - .top(scaled(174)) + .topToBottomOfView(navView) .edgesHorzontal() .bottomToTopOfView(sendButton, offset: -scaled(12)) let contentView = UIView() - contentView.backgroundColor = UIColor(hexStr: "#F9F9F9") + contentView.backgroundColor = .clear scrollView.addSubview(contentView) contentView.layoutChain.edges().widthToView(scrollView) @@ -248,10 +281,10 @@ final class PigeonMessageView: UIView, UITextFieldDelegate { sectionSparkle.contentMode = .scaleAspectFit contentView.addSubview(sectionSparkle) sectionSparkle.layoutChain - .top(scaled(21)) - .left(scaled(28)) - .width(scaled(11)) - .height(scaled(11)) + .top(12) + .left(27) + .width(11) + .height(11) let sectionTitle = UILabel() sectionTitle.text = "选择收件人" @@ -259,63 +292,79 @@ final class PigeonMessageView: UIView, UITextFieldDelegate { sectionTitle.textColor = UIColor(hexStr: "#293445") contentView.addSubview(sectionTitle) sectionTitle.layoutChain - .top(scaled(16)) - .leftToRightOfView(sectionSparkle, offset: scaled(8)) + .centerY(sectionSparkle) + .leftToRightOfView(sectionSparkle, offset: 8) switchGroupButton.backgroundColor = .white - switchGroupButton.setTitle(" 切换圈子 ", for: .normal) - switchGroupButton.titleLabel?.font = .systemFont(ofSize: 12, weight: .medium) - switchGroupButton.setTitleColor(UIColor(hexStr: "#293445"), for: .normal) - switchGroupButton.layer.borderWidth = 1 - switchGroupButton.layer.borderColor = UIColor(hexStr: "#E8E8E8").cgColor - switchGroupButton.cornerRadius = 14 + switchGroupButton.cornerRadius = 8 + switchGroupButton.clipsToBounds = true + setCurrentGroupName("") contentView.addSubview(switchGroupButton) switchGroupButton.layoutChain .centerY(sectionTitle) - .right(scaled(15)) - .height(28) + .right(15) + .width(68, relation: .greaterThanOrEqual) + .height(24) let recipientCard = UIView() recipientCard.backgroundColor = .white - recipientCard.layer.cornerRadius = scaled(20) + recipientCard.layer.cornerRadius = 20 contentView.addSubview(recipientCard) recipientCard.layoutChain - .topToBottomOfView(sectionTitle, offset: scaled(8)) - .edgesHorzontal(scaled(15)) - .height(100) + .topToBottomOfView(sectionTitle, offset: 14) + .edgesHorzontal(15) + .height(86) setupRecipientCard(recipientCard) - let modeContainer = UIView() - modeContainer.backgroundColor = .white - modeContainer.layer.cornerRadius = scaled(22) - contentView.addSubview(modeContainer) - modeContainer.layoutChain - .topToBottomOfView(recipientCard, offset: scaled(12)) - .edgesHorzontal(scaled(15)) - .height(scaled(44)) - setupModeButtons(modeContainer) - contentView.addSubview(imageEditorView) - contentView.addSubview(voiceEditorView) imageEditorView.layoutChain - .topToBottomOfView(modeContainer, offset: scaled(12)) - .edgesHorzontal(scaled(15)) - .height(scaled(190)) - voiceEditorView.layoutChain - .topToBottomOfView(modeContainer, offset: scaled(12)) - .edgesHorzontal(scaled(15)) - .height(scaled(190)) + .topToBottomOfView(recipientCard, offset: 14) + .edgesHorzontal(15) + .heightToWidth(200.0 / 345.0) setupImageEditor() - setupVoiceEditor() let thumbnailRow = UIView() contentView.addSubview(thumbnailRow) thumbnailRow.layoutChain - .topToBottomOfView(imageEditorView, offset: scaled(14)) - .edgesHorzontal(scaled(17)) - .height(scaled(60)) - .bottom(scaled(20)) + .topToBottomOfView(imageEditorView, offset: 12) + .edgesHorzontal() + .height(72) setupThumbnailRow(thumbnailRow) + + let modeContainer = UIView() + modeContainer.backgroundColor = .white + modeContainer.layer.cornerRadius = 10 + contentView.addSubview(modeContainer) + modeContainer.layoutChain + .topToBottomOfView(thumbnailRow, offset: 14) + .centerX() + .width(152) + .height(36) + setupModeButtons(modeContainer) + + setupCopyFooter() + voiceChipPicker.onSelectRecord = { [weak self] in + self?.onVoiceTemplateSelected?(nil) + self?.updateMode() + } + voiceChipPicker.onSelectTemplate = { [weak self] template in + self?.onVoiceTemplateSelected?(template) + self?.updateMode() + } + setupVoiceEditor() + + let bottomStack = UIStackView(arrangedSubviews: [copyFooter, voiceChipPicker, voiceEditorView]) + bottomStack.axis = .vertical + bottomStack.spacing = 12 + bottomStack.alignment = .fill + contentView.addSubview(bottomStack) + copyFooter.layoutChain.height(40) + voiceChipPicker.layoutChain.height(56) + voiceEditorView.layoutChain.height(110) + bottomStack.layoutChain + .topToBottomOfView(modeContainer, offset: 12) + .edgesHorzontal(15) + .bottom(16) } private func setupRecipientCard(_ card: UIView) { @@ -323,19 +372,26 @@ final class PigeonMessageView: UIView, UITextFieldDelegate { memberCollectionView.layoutChain .edgesHorzontal(8) .centerY() - .height(84) + .height(76) + + card.addSubview(recipientFadeView) + recipientFadeView.isUserInteractionEnabled = false + recipientFadeView.layoutChain + .right() + .edgesVertical() + .width(30) } private func setupModeButtons(_ container: UIView) { configureModeButton( imageModeButton, - title: "图片", - imageName: "PigeonMessage/gallery" + title: "文案", + imageName: "PigeonMessage/copy" ) configureModeButton( voiceModeButton, title: "语音", - imageName: "PigeonMessage/microphone" + imageName: "PigeonMessage/mic_tab" ) container.addSubview(imageModeButton) container.addSubview(voiceModeButton) @@ -353,14 +409,17 @@ final class PigeonMessageView: UIView, UITextFieldDelegate { private func configureModeButton(_ button: UIButton, title: String, imageName: String) { var titleAttributes = AttributeContainer() - titleAttributes.font = FontManager.boboBold(scaled(16)) + titleAttributes.font = .systemFont(ofSize: 14, weight: .bold) var configuration = UIButton.Configuration.plain() configuration.attributedTitle = AttributedString(title, attributes: titleAttributes) configuration.image = UIImage(named: imageName)?.withRenderingMode(.alwaysTemplate) - configuration.imagePadding = scaled(6) + configuration.imagePadding = 4 configuration.contentInsets = .zero + configuration.cornerStyle = .fixed + configuration.background.cornerRadius = 10 button.configuration = configuration - button.layer.cornerRadius = scaled(18) + button.layer.cornerRadius = 10 + button.clipsToBounds = true } private func setupImageEditor() { @@ -370,16 +429,13 @@ final class PigeonMessageView: UIView, UITextFieldDelegate { selectedImageMaskView.backgroundColor = .black.withAlphaComponent(0.2) selectedImageMaskView.isHidden = true - selectedImageView.contentMode = .scaleAspectFit + selectedImageView.contentMode = .scaleAspectFill selectedImageView.clipsToBounds = false imageEditorView.addSubview(selectedImageView) - selectedImageView.layoutChain - .top() - .left() - .right() - let imageHeight = selectedImageView.heightAnchor.constraint(equalTo: imageEditorView.heightAnchor) - imageHeight.isActive = true - selectedImageHeightConstraint = imageHeight + selectedImageView.layoutChain.edges() +// let imageHeight = selectedImageView.heightAnchor.constraint(equalTo: imageEditorView.heightAnchor) +// imageHeight.isActive = true +// selectedImageHeightConstraint = imageHeight imageEditorView.addSubview(selectedImageMaskView) selectedImageMaskView.layoutChain.edges() @@ -391,206 +447,107 @@ final class PigeonMessageView: UIView, UITextFieldDelegate { imageEditorView.addSubview(imagePlaceholderLabel) imagePlaceholderLabel.layoutChain.centerX().centerY() - let captionBackground = UIView() - captionBackground.backgroundColor = UIColor.white.withAlphaComponent(0.9) - captionBackground.layer.cornerRadius = scaled(12) - imageEditorView.addSubview(captionBackground) - captionBackground.layoutChain - .left(scaled(10)) - .right(scaled(60)) - .bottom(scaled(10)) - .height(scaled(40)) + overlayCaptionBar.backgroundColor = UIColor.white.withAlphaComponent(0.9) + overlayCaptionBar.layer.cornerRadius = 12 + overlayCaptionBar.isHidden = true + imageEditorView.addSubview(overlayCaptionBar) + overlayCaptionBar.layoutChain + .left(10) + .right(10) + .bottom(10) + .height(40) + overlayCaptionLabel.font = .systemFont(ofSize: 14) + overlayCaptionLabel.textColor = UIColor(hexStr: "#293445") + overlayCaptionBar.addSubview(overlayCaptionLabel) + overlayCaptionLabel.layoutChain.edgesHorzontal(14).centerY() + playbackBar.backgroundColor = UIColor.white.withAlphaComponent(0.92) + playbackBar.layer.cornerRadius = 12 + playbackBar.isHidden = true + imageEditorView.addSubview(playbackBar) + playbackBar.layoutChain + .left(10) + .right(10) + .bottom(10) + .height(40) + setPlaying(false) + playPauseButton.isEnabled = false + playbackBar.addSubview(playPauseButton) + playPauseButton.layoutChain.left(8).centerY().width(28).height(28) + playbackBar.addSubview(waveformView) + voiceTimeLabel.text = "00″/10″" + voiceTimeLabel.font = .systemFont(ofSize: 16) + voiceTimeLabel.textColor = UIColor(hexStr: "#13B6F1") + playbackBar.addSubview(voiceTimeLabel) + voiceTimeLabel.layoutChain.right(10).centerY() + waveformView.layoutChain + .leftToRightOfView(playPauseButton, offset: 8) + .centerY() + .rightToLeftOfView(voiceTimeLabel, offset: -2) + .height(24) + } + + private func setupCopyFooter() { captionField.attributedPlaceholder = NSAttributedString( string: "随便说点什么吧~", attributes: [.foregroundColor: UIColor(hexStr: "#A7ABB2")] ) - captionField.font = .systemFont(ofSize: scaled(14)) + captionField.font = .systemFont(ofSize: 14) captionField.textColor = UIColor(hexStr: "#293445") captionField.delegate = self + captionField.returnKeyType = .done captionField.addTarget(self, action: #selector(captionChanged), for: .editingChanged) - captionBackground.addSubview(captionField) - captionBackground.addSubview(captionCountLabel) - captionField.layoutChain - .left(scaled(14)) - .centerY() - .right(scaled(48)) - .height(scaled(34)) + let fieldBg = UIView() + fieldBg.backgroundColor = .white + fieldBg.layer.cornerRadius = 12 + fieldBg.layer.borderWidth = 1 + fieldBg.layer.borderColor = UIColor(hexStr: "#EEEEEE").cgColor + copyFooter.addSubview(fieldBg) + fieldBg.layoutChain.left().right(52).edgesVertical() + fieldBg.addSubview(captionField) + fieldBg.addSubview(captionCountLabel) + captionField.layoutChain.left(14).centerY().right(48).height(34) captionCountLabel.text = "0/20" - captionCountLabel.font = .systemFont(ofSize: scaled(14)) + captionCountLabel.font = .systemFont(ofSize: 14) captionCountLabel.textColor = UIColor(hexStr: "#A7ABB2") - captionCountLabel.layoutChain.right(scaled(10)).centerY() + captionCountLabel.layoutChain.right(10).centerY() deleteImageButton.setImage(UIImage(named: "PigeonMessage/trash"), for: .normal) - deleteImageButton.backgroundColor = UIColor.white.withAlphaComponent(0.92) - deleteImageButton.layer.cornerRadius = scaled(12) - imageEditorView.addSubview(deleteImageButton) - deleteImageButton.layoutChain - .right(scaled(10)) - .bottom(scaled(10)) - .width(scaled(40)) - .height(scaled(40)) + deleteImageButton.backgroundColor = .white + deleteImageButton.layer.cornerRadius = 12 + copyFooter.addSubview(deleteImageButton) + deleteImageButton.layoutChain.right().centerY().width(40).height(40) } private func setupVoiceEditor() { - voiceEditorView.backgroundColor = .white - voiceEditorView.layer.cornerRadius = scaled(22) + voiceEditorView.backgroundColor = .clear - recordOuterHaloView.backgroundColor = UIColor(hexStr: "#F2FBFF") - recordOuterHaloView.layer.cornerRadius = scaled(46) - voiceEditorView.addSubview(recordOuterHaloView) - recordOuterHaloView.layoutChain - .top(scaled(15)) - .centerX() - .width(scaled(92)) - .height(scaled(92)) - - recordInnerHaloView.backgroundColor = UIColor(hexStr: "#E6F7FF") - recordInnerHaloView.layer.cornerRadius = scaled(37) - voiceEditorView.addSubview(recordInnerHaloView) - recordInnerHaloView.layoutChain - .centerX(recordOuterHaloView) - .centerY(recordOuterHaloView) - .width(scaled(74)) - .height(scaled(74)) - - recordButton.setImage(UIImage(named: "PigeonMessage/record_microphone"), for: .normal) + recordButton.setImage(UIImage(named: "PigeonMessage/record_btn"), for: .normal) voiceEditorView.addSubview(recordButton) recordButton.layoutChain - .centerX(recordOuterHaloView) - .centerY(recordOuterHaloView) - .width(scaled(92)) - .height(scaled(92)) + .top() + .centerX() + .width(55) + .height(55) - recordingHintLabel.text = "按住录音 松开发送" - recordingHintLabel.font = .systemFont(ofSize: scaled(14)) + recordingHintLabel.text = "按住录音 松开发送" + recordingHintLabel.font = .systemFont(ofSize: 13) recordingHintLabel.textColor = UIColor(hexStr: "#A7ABB2") voiceEditorView.addSubview(recordingHintLabel) recordingHintLabel.layoutChain - .topToBottomOfView(recordOuterHaloView, offset: scaled(5)) + .topToBottomOfView(recordButton, offset: 6) .centerX() - - let playbackBar = UIView() - playbackBar.backgroundColor = UIColor(hexStr: "#F5F5F6") - playbackBar.layer.cornerRadius = scaled(12) - voiceEditorView.addSubview(playbackBar) - playbackBar.layoutChain - .left(scaled(10)) - .right(scaled(10)) - .bottom(scaled(10)) - .height(scaled(40)) - - setPlaying(false) - playPauseButton.isEnabled = false - playbackBar.addSubview(playPauseButton) - playPauseButton.layoutChain - .left(scaled(8)) - .centerY() - .width(scaled(28)) - .height(scaled(28)) - - playbackBar.addSubview(waveformView) - waveformView.layoutChain - .leftToRightOfView(playPauseButton, offset: scaled(8)) - .centerY() - .right(scaled(66)) - .height(scaled(24)) - - voiceTimeLabel.text = "00″/10″" - voiceTimeLabel.font = .systemFont(ofSize: scaled(16)) - voiceTimeLabel.textColor = UIColor(hexStr: "#13B6F1") - playbackBar.addSubview(voiceTimeLabel) - voiceTimeLabel.layoutChain.right(scaled(10)).centerY() } private func setupThumbnailRow(_ row: UIView) { - imageThumbnailButton.layer.cornerRadius = scaled(20) - imageThumbnailButton.layer.borderWidth = scaled(2) - imageThumbnailButton.layer.borderColor = UIColor(hexStr: "#09B6F4").cgColor - imageThumbnailButton.clipsToBounds = true - row.addSubview(imageThumbnailButton) - imageThumbnailButton.layoutChain - .left() - .top() - .width(scaled(60)) - .height(scaled(60)) - - imageThumbnailView.contentMode = .scaleAspectFill - imageThumbnailView.isUserInteractionEnabled = false - imageThumbnailButton.addSubview(imageThumbnailView) - imageThumbnailView.layoutChain.edges() - - let tick = UIImageView(image: UIImage(named: "PigeonMessage/tick_circle")) - imageThumbnailButton.addSubview(tick) - tick.layoutChain - .right(scaled(6)) - .bottom(scaled(5)) - .width(scaled(16)) - .height(scaled(16)) - - addTile.backgroundColor = UIColor(hexStr: "#EBECEE") - addTile.layer.cornerRadius = scaled(20) - row.addSubview(addTile) - addTile.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - addTile.topAnchor.constraint(equalTo: row.topAnchor), - addTile.widthAnchor.constraint(equalToConstant: scaled(60)), - addTile.heightAnchor.constraint(equalToConstant: scaled(60)) - ]) - addTileLeadingWithImage = addTile.leadingAnchor.constraint( - equalTo: imageThumbnailButton.trailingAnchor, - constant: scaled(10) - ) - addTileLeadingWithoutImage = addTile.leadingAnchor.constraint(equalTo: row.leadingAnchor) - addTileLeadingWithoutImage?.isActive = true - addTile.addSubview(addImageButton) - addImageButton.layoutChain.edges() - let addIcon = UIImageView(image: UIImage(named: "PigeonMessage/add")) - addIcon.contentMode = .scaleAspectFit - let addLabel = UILabel() - addLabel.text = "添加" - addLabel.font = .systemFont(ofSize: scaled(13)) - addLabel.textColor = UIColor(hexStr: "#ADB1B8") - addTile.addSubview(addIcon) - addTile.addSubview(addLabel) - addIcon.layoutChain - .top(scaled(8)) - .centerX() - .width(scaled(24)) - .height(scaled(24)) - addLabel.layoutChain - .topToBottomOfView(addIcon, offset: scaled(2)) - .centerX() - - voiceThumbnailView.backgroundColor = UIColor(hexStr: "#F2F3F4") - voiceThumbnailView.layer.cornerRadius = scaled(20) - voiceThumbnailView.layer.borderWidth = scaled(2) - voiceThumbnailView.layer.borderColor = UIColor(hexStr: "#09B6F4").cgColor - row.addSubview(voiceThumbnailView) - voiceThumbnailView.layoutChain - .left() - .top() - .width(scaled(60)) - .height(scaled(60)) - let mic = UIImageView( - image: UIImage(named: "PigeonMessage/microphone_thumb")?.withRenderingMode(.alwaysTemplate) - ) - mic.tintColor = UIColor(hexStr: "#AEB3BA") - mic.contentMode = .scaleAspectFit - let voiceLabel = UILabel() - voiceLabel.text = "录音" - voiceLabel.font = .systemFont(ofSize: scaled(13)) - voiceLabel.textColor = UIColor(hexStr: "#969CA5") - voiceThumbnailView.addSubview(mic) - voiceThumbnailView.addSubview(voiceLabel) - mic.layoutChain - .top(scaled(7)) - .centerX() - .width(scaled(24)) - .height(scaled(24)) - voiceLabel.layoutChain - .topToBottomOfView(mic, offset: scaled(2)) - .centerX() + row.addSubview(templatePickerView) + templatePickerView.layoutChain.edgesHorzontal(15).edgesVertical() + templatePickerView.onUploadTapped = { [weak self] in + self?.onUploadTapped?() + } + templatePickerView.onTemplateSelected = { [weak self] template in + self?.onTemplateSelected?(template) + } } @objc private func selectImageMode() { @@ -607,6 +564,7 @@ final class PigeonMessageView: UIView, UITextFieldDelegate { captionField.text = String(text.prefix(20)) } captionCountLabel.text = "\((captionField.text ?? "").count)/20" + updatePreviewOverlays() } func textField( @@ -619,9 +577,16 @@ final class PigeonMessageView: UIView, UITextFieldDelegate { return current.replacingCharacters(in: stringRange, with: string).count <= 20 } + func textFieldShouldReturn(_ textField: UITextField) -> Bool { + textField.resignFirstResponder() + return true + } + override func layoutSubviews() { super.layoutSubviews() - updateSelectedImageTopFill() + let contentWidth = CGFloat(members.count) * memberLayout.itemSize.width + recipientFadeView.isHidden = contentWidth <= memberCollectionView.bounds.width +// updateSelectedImageTopFill() } private func updateSelectedImageTopFill() { @@ -644,34 +609,37 @@ final class PigeonMessageView: UIView, UITextFieldDelegate { selectedImageHeightConstraint = constraint } - private func updateAddTilePosition(hasImage: Bool) { - addTileLeadingWithImage?.isActive = false - addTileLeadingWithoutImage?.isActive = false - if hasImage { - addTileLeadingWithImage?.isActive = true - } else { - addTileLeadingWithoutImage?.isActive = true - } + private func updateMode() { + let isCopy = mode == .image + copyFooter.isHidden = !isCopy + voiceChipPicker.isHidden = isCopy + let usingVoiceTemplate = voiceChipPicker.selectedTemplate != nil + voiceEditorView.isHidden = isCopy || usingVoiceTemplate + styleModeButton(imageModeButton, selected: isCopy) + styleModeButton(voiceModeButton, selected: !isCopy) + updatePreviewOverlays() } - private func updateMode() { - let isImage = mode == .image - imageEditorView.isHidden = !isImage - voiceEditorView.isHidden = isImage - imageThumbnailButton.isHidden = !isImage || selectedImageView.image == nil - addTile.isHidden = !isImage - voiceThumbnailView.isHidden = isImage - styleModeButton(imageModeButton, selected: isImage) - styleModeButton(voiceModeButton, selected: !isImage) + private func updatePreviewOverlays() { + let caption = captionText.trimmingCharacters(in: .whitespacesAndNewlines) + overlayCaptionLabel.text = caption + overlayCaptionBar.isHidden = mode != .image || caption.isEmpty + let hasVoiceContent = playPauseButton.isEnabled || voiceChipPicker.selectedTemplate != nil + playbackBar.isHidden = mode != .voice || !hasVoiceContent } private func styleModeButton(_ button: UIButton, selected: Bool) { - button.backgroundColor = selected ? UIColor(hexStr: "#45C2FF") : .clear let foregroundColor = selected ? UIColor.white : UIColor(hexStr: "#A5A5A5") - var configuration = button.configuration - configuration?.baseForegroundColor = foregroundColor + var configuration = button.configuration ?? .plain() + configuration.baseForegroundColor = foregroundColor + configuration.cornerStyle = .fixed + configuration.background.backgroundColor = selected ? UIColor(hexStr: "#16B3FF") : .clear + configuration.background.cornerRadius = 10 button.configuration = configuration + button.backgroundColor = .clear button.tintColor = foregroundColor + button.layer.cornerRadius = 10 + button.clipsToBounds = true } } @@ -687,9 +655,6 @@ extension PigeonMessageView: UICollectionViewDataSource, UICollectionViewDelegat ) as! PigeonMemberCell let member = members[indexPath.item] cell.configure(member: member, selected: selectedMemberIds.contains(member.user_id)) - cell.onToggle = { [weak self] in - self?.toggleMember(at: indexPath.item) - } return cell } @@ -715,48 +680,49 @@ private extension PigeonMessageView { private final class PigeonMemberCell: UICollectionViewCell { static let reuseId = "PigeonMemberCell" - var onToggle: (() -> Void)? - + private let avatarContainer = UIView() private let avatarView = UIImageView() private let onlineDot = UIView() private let nameLabel = UILabel() - private let checkBtn = UIButton(type: .custom) override init(frame: CGRect) { super.init(frame: frame) + + avatarContainer.layer.cornerRadius = 12 + avatarContainer.clipsToBounds = false + contentView.addSubview(avatarContainer) + avatarContainer.layoutChain.top().centerX().width(42).height(42) + avatarView.contentMode = .scaleAspectFill avatarView.clipsToBounds = true avatarView.layer.cornerRadius = 10 - contentView.addSubview(avatarView) - avatarView.layoutChain.top(5).centerX().width(38).height(38) + avatarContainer.addSubview(avatarView) + avatarView.layoutChain.edges(all: 2) - onlineDot.layer.cornerRadius = 4 + onlineDot.layer.cornerRadius = 4.5 onlineDot.layer.borderWidth = 1.5 onlineDot.layer.borderColor = UIColor.white.cgColor onlineDot.isUserInteractionEnabled = false contentView.addSubview(onlineDot) - onlineDot.layoutChain.topToView(avatarView, offset: -1).rightToView(avatarView, offset: 1).width(8).height(8) + onlineDot.layoutChain + .topToView(avatarContainer, offset: -1) + .rightToView(avatarContainer, offset: 1) + .width(9) + .height(9) - nameLabel.font = .systemFont(ofSize: 8, weight: .medium) - nameLabel.textColor = UIColor(hexStr: "#767676") + nameLabel.font = .systemFont(ofSize: 10, weight: .medium) + nameLabel.textColor = UIColor(hexStr: "#293445") nameLabel.textAlignment = .center nameLabel.lineBreakMode = .byTruncatingTail - nameLabel.backgroundColor = UIColor(hexStr: "#F8F8F8") - nameLabel.layer.cornerRadius = 7 + nameLabel.backgroundColor = UIColor(hexStr: "#F7F7F8") + nameLabel.layer.cornerRadius = 10 nameLabel.clipsToBounds = true contentView.addSubview(nameLabel) nameLabel.layoutChain - .topToBottomOfView(avatarView, offset: 4) + .topToBottomOfView(avatarContainer, offset: 5) .centerX() - .width(56) - .height(14) - - checkBtn.setImage(UIImage(named: "Common/checkbox"), for: .normal) - checkBtn.setImage(UIImage(named: "Common/checkbox_on"), for: .selected) - checkBtn.imageView?.contentMode = .scaleAspectFit - checkBtn.addTarget(self, action: #selector(tapCheck), for: .touchUpInside) - contentView.addSubview(checkBtn) - checkBtn.layoutChain.topToBottomOfView(nameLabel, offset: 2).centerX().width(20).height(20) + .width(58) + .height(20) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -767,11 +733,329 @@ private final class PigeonMemberCell: UICollectionViewCell { : UIImage(named: "Common/default_avatar") nameLabel.text = member.nick_name onlineDot.backgroundColor = UIColor(hexStr: member.is_online ? "#67E151" : "#AAAAAA") - checkBtn.isSelected = selected + avatarContainer.layer.borderWidth = selected ? 2 : 0 + avatarContainer.layer.borderColor = UIColor(hexStr: "#00ADFE").cgColor + } +} + +private final class PigeonRecipientFadeView: UIView { + private let gradientLayer = CAGradientLayer() + + override init(frame: CGRect) { + super.init(frame: frame) + gradientLayer.colors = [ + UIColor.white.withAlphaComponent(0).cgColor, + UIColor.white.cgColor + ] + gradientLayer.startPoint = CGPoint(x: 0, y: 0.5) + gradientLayer.endPoint = CGPoint(x: 1, y: 0.5) + layer.addSublayer(gradientLayer) } - @objc private func tapCheck() { - onToggle?() + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layoutSubviews() { + super.layoutSubviews() + gradientLayer.frame = bounds + } +} + +private final class PigeonHeaderGradientView: UIView { + private let gradientLayer = CAGradientLayer() + + override init(frame: CGRect) { + super.init(frame: frame) + gradientLayer.colors = [ + UIColor(hexStr: "#BAF4FF").cgColor, + UIColor(hexStr: "#BAF4FF").withAlphaComponent(0).cgColor + ] + gradientLayer.locations = [0, 1] + gradientLayer.startPoint = CGPoint(x: 0.5, y: 0) + gradientLayer.endPoint = CGPoint(x: 0.5, y: 1) + layer.addSublayer(gradientLayer) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layoutSubviews() { + super.layoutSubviews() + gradientLayer.frame = bounds + } +} + +private final class PigeonVoiceChipPickerView: UIView, + UICollectionViewDataSource, + UICollectionViewDelegateFlowLayout { + var onSelectRecord: (() -> Void)? + var onSelectTemplate: ((PigeonVoiceTemplate) -> Void)? + private(set) var selectedTemplate: PigeonVoiceTemplate? + private let templates = PigeonVoiceTemplate.templates + + private lazy var collectionView: UICollectionView = { + let layout = UICollectionViewFlowLayout() + layout.scrollDirection = .horizontal + layout.minimumLineSpacing = 8 + layout.itemSize = CGSize(width: 92, height: 52) + let view = UICollectionView(frame: .zero, collectionViewLayout: layout) + view.backgroundColor = .clear + view.showsHorizontalScrollIndicator = false + view.dataSource = self + view.delegate = self + view.register(PigeonVoiceChipCell.self, forCellWithReuseIdentifier: PigeonVoiceChipCell.reuseID) + view.contentInset = UIEdgeInsets(top: 0, left: 15, bottom: 0, right: 15) + return view + }() + + override init(frame: CGRect) { + super.init(frame: frame) + addSubview(collectionView) + collectionView.layoutChain.edges() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + templates.count + 1 + } + + func collectionView( + _ collectionView: UICollectionView, + cellForItemAt indexPath: IndexPath + ) -> UICollectionViewCell { + let cell = collectionView.dequeueReusableCell( + withReuseIdentifier: PigeonVoiceChipCell.reuseID, + for: indexPath + ) as! PigeonVoiceChipCell + if indexPath.item == 0 { + cell.configureRecord(selected: selectedTemplate == nil) + } else { + let template = templates[indexPath.item - 1] + cell.configure(template: template, selected: selectedTemplate?.id == template.id) + } + return cell + } + + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + if indexPath.item == 0 { + selectedTemplate = nil + collectionView.reloadData() + onSelectRecord?() + return + } + let template = templates[indexPath.item - 1] + selectedTemplate = template + collectionView.reloadData() + onSelectTemplate?(template) + } +} + +private final class PigeonVoiceChipCell: UICollectionViewCell { + static let reuseID = "PigeonVoiceChipCell" + private let iconView = UIImageView() + private let titleLab = UILabel() + private let subLab = UILabel() + + override init(frame: CGRect) { + super.init(frame: frame) + contentView.backgroundColor = .white + contentView.layer.cornerRadius = 14 + contentView.layer.borderWidth = 1 + contentView.layer.borderColor = UIColor(hexStr: "#E8E8E8").cgColor + contentView.addSubview(iconView) + contentView.addSubview(titleLab) + contentView.addSubview(subLab) + iconView.contentMode = .scaleAspectFit + iconView.layoutChain.left(8).centerY().width(18).height(18) + titleLab.font = .systemFont(ofSize: 13, weight: .medium) + titleLab.textColor = UIColor(hexStr: "#293445") + titleLab.layoutChain.leftToRightOfView(iconView, offset: 6).top(8).right(8) + subLab.font = .systemFont(ofSize: 11) + subLab.textColor = UIColor(hexStr: "#A7ABB2") + subLab.layoutChain.leftToView(titleLab).topToBottomOfView(titleLab, offset: 2) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func configureRecord(selected: Bool) { + iconView.image = UIImage(named: "PigeonMessage/voice_chip_mic") + titleLab.text = "录音" + subLab.text = "说点什么吧" + applySelected(selected) + } + + func configure(template: PigeonVoiceTemplate, selected: Bool) { + iconView.image = UIImage(named: "PigeonMessage/voice_chip_play") + titleLab.text = template.title + subLab.text = String(format: "%02d:%02d", Int(template.duration) / 60, Int(template.duration) % 60) + applySelected(selected) + } + + private func applySelected(_ selected: Bool) { + contentView.layer.borderColor = UIColor(hexStr: selected ? "#00ADFE" : "#E8E8E8").cgColor + contentView.layer.borderWidth = selected ? 1.5 : 1 + } +} + +private final class PigeonTemplatePickerView: UIView, + UICollectionViewDataSource, + UICollectionViewDelegateFlowLayout { + var onUploadTapped: (() -> Void)? + var onTemplateSelected: ((PigeonLocalTemplate) -> Void)? + + private let templates = PigeonLocalTemplate.imageTemplates + private var selectedTemplateID: String? + private var isCustomImageSelected = false + + private lazy var collectionView: UICollectionView = { + let layout = UICollectionViewFlowLayout() + layout.scrollDirection = .horizontal + layout.minimumLineSpacing = 10 + layout.minimumInteritemSpacing = 10 + layout.itemSize = CGSize(width: 48, height: 72) + let view = UICollectionView(frame: .zero, collectionViewLayout: layout) + view.backgroundColor = .clear + view.showsHorizontalScrollIndicator = false + view.alwaysBounceHorizontal = true + view.contentInset = .zero + view.dataSource = self + view.delegate = self + view.register( + PigeonTemplateCell.self, + forCellWithReuseIdentifier: PigeonTemplateCell.reuseID + ) + return view + }() + + override init(frame: CGRect) { + super.init(frame: frame) + addSubview(collectionView) + collectionView.layoutChain.edges() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func setSelection(templateID: String?, customImageSelected: Bool) { + selectedTemplateID = templateID + isCustomImageSelected = customImageSelected + collectionView.reloadData() + guard let templateID, + let index = templates.firstIndex(where: { $0.id == templateID }) else { + return + } + collectionView.scrollToItem( + at: IndexPath(item: index + 1, section: 0), + at: .centeredHorizontally, + animated: true + ) + } + + func collectionView( + _ collectionView: UICollectionView, + numberOfItemsInSection section: Int + ) -> Int { + templates.count + 1 + } + + func collectionView( + _ collectionView: UICollectionView, + cellForItemAt indexPath: IndexPath + ) -> UICollectionViewCell { + let cell = collectionView.dequeueReusableCell( + withReuseIdentifier: PigeonTemplateCell.reuseID, + for: indexPath + ) as! PigeonTemplateCell + if indexPath.item == 0 { + cell.configureAsUpload(selected: isCustomImageSelected) + } else { + let template = templates[indexPath.item - 1] + cell.configure( + template: template, + selected: selectedTemplateID == template.id + ) + } + return cell + } + + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + if indexPath.item == 0 { + onUploadTapped?() + return + } + let template = templates[indexPath.item - 1] + selectedTemplateID = template.id + isCustomImageSelected = false + collectionView.reloadData() + onTemplateSelected?(template) + } +} + +private final class PigeonTemplateCell: UICollectionViewCell { + static let reuseID = "PigeonTemplateCell" + + private let imageContainer = UIView() + private let imageView = UIImageView() + private let addImageView = UIImageView(image: UIImage(named: "PigeonMessage/add")) + private let titleLabel = UILabel() + + override init(frame: CGRect) { + super.init(frame: frame) + + imageContainer.backgroundColor = UIColor(hexStr: "#EBECEE") + imageContainer.layer.cornerRadius = 18 + imageContainer.clipsToBounds = true + contentView.addSubview(imageContainer) + imageContainer.layoutChain.top().left().right().height(48) + + imageView.contentMode = .scaleAspectFill + imageView.clipsToBounds = true + imageContainer.addSubview(imageView) + imageView.layoutChain.edges() + + addImageView.contentMode = .scaleAspectFit + imageContainer.addSubview(addImageView) + addImageView.layoutChain.centerX().centerY().width(28).height(28) + + titleLabel.font = .systemFont(ofSize: 12, weight: .medium) + titleLabel.textColor = UIColor(hexStr: "#293445") + titleLabel.textAlignment = .center + titleLabel.lineBreakMode = .byTruncatingTail + contentView.addSubview(titleLabel) + titleLabel.layoutChain.topToBottomOfView(imageContainer, offset: 4).left().right().height(20) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func configureAsUpload(selected: Bool) { + imageView.image = nil + imageView.isHidden = true + addImageView.isHidden = false + titleLabel.text = "上传图片" + applySelection(selected) + } + + func configure(template: PigeonLocalTemplate, selected: Bool) { + imageView.image = template.coverImage + imageView.isHidden = false + addImageView.isHidden = true + titleLabel.text = template.title + applySelection(selected) + } + + private func applySelection(_ selected: Bool) { + imageContainer.layer.borderWidth = selected ? 2 : 0 + imageContainer.layer.borderColor = UIColor(hexStr: "#00ADFE").cgColor } } @@ -864,4 +1148,3 @@ final class PigeonWaveformView: UIView { } } } - diff --git a/QuickLocation/Service/PigeonService.swift b/QuickLocation/Service/PigeonService.swift index d81c0c51..a5174f83 100644 --- a/QuickLocation/Service/PigeonService.swift +++ b/QuickLocation/Service/PigeonService.swift @@ -10,16 +10,18 @@ struct PigeonService { groupKey: String, toUsers: [String], msgType: Int, - fileId: String, - textMsg: String + msg: String, + bgImg: String, + extra: [String: Any]? = nil ) -> Observable { let api = PigeonAPI.send( groupKey: groupKey, toUsers: toUsers, msgType: msgType, - msg: fileId, - textMsg: textMsg, - expireTime: 0 + msg: msg, + bgImg: bgImg, + expireTime: 0, + extra: extra ).multiTarget return APIProvider.request(token: api, handle: false) .map(ResponseModel.self) diff --git a/QuickLocation/Service/UserService.swift b/QuickLocation/Service/UserService.swift index ab81e935..2ed52ff4 100644 --- a/QuickLocation/Service/UserService.swift +++ b/QuickLocation/Service/UserService.swift @@ -187,4 +187,12 @@ struct UserService { .map(ResponseModel.self) .asObservable() } + + /// 关系列表 + static func relations() -> Observable { + let api = UserAPI.relations.multiTarget + return APIProvider.request(token: api, handle: false) + .map(RelationListResponse.self) + .asObservable() + } } diff --git a/QuickLocation/UIKit/BaseNavigationView.swift b/QuickLocation/UIKit/BaseNavigationView.swift index 0c88dd38..847be04d 100644 --- a/QuickLocation/UIKit/BaseNavigationView.swift +++ b/QuickLocation/UIKit/BaseNavigationView.swift @@ -39,8 +39,12 @@ class BaseNavigationView: UIView { .width(44) .height(44) + titleLabel.translatesAutoresizingMaskIntoConstraints = false + titleLabel.topAnchor.constraint( + equalTo: safeAreaLayoutGuide.topAnchor, + constant: 12 + ).isActive = true titleLabel.layoutChain - .top(kStatusBarHeight + 12) .centerY(backBtn) .centerX()