- 修改bug、联调接口

- UI调整
This commit is contained in:
linshujie 2026-09-09 18:29:42 +08:00
parent b2ccc36d15
commit f0a0b3d33f
98 changed files with 1213 additions and 276 deletions

View File

@ -2972,7 +2972,6 @@
SUPPORTS_MACCATALYST = NO;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "AdHoc $(inherited)";
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
SWIFT_EMIT_LOC_STRINGS = YES;

View File

@ -22,6 +22,8 @@ enum GroupAPI {
/// - opType: createdismisssetdefault
/// - requestData[String: Any]
case operate(opType: String, requestData: [String: Any])
///
case privilegeRemind(title: String, content: String, groupKey: String, userId: String)
}
extension GroupAPI: MultiTargetProtocol {
@ -36,6 +38,8 @@ extension GroupAPI: MultiTargetProtocol {
return "mapi/group/users"
case .operate:
return "mapi/group/operate"
case .privilegeRemind:
return "mapi/group/privilege/remind"
}
}
@ -64,6 +68,14 @@ extension GroupAPI: MultiTargetProtocol {
params["op_type"] = opType
params.merge(requestData) { _, new in new }
return .requestParameters(parameters: params, encoding: JSONEncoding())
case let .privilegeRemind(title, content, groupKey, userId):
var params = Parameters()
params["title"] = title
params["content"] = content
params["group_key"] = groupKey
params["user_id"] = userId
return .requestParameters(parameters: params, encoding: JSONEncoding())
}
}
}

View File

@ -228,7 +228,7 @@ extension UserAPI: MultiTargetProtocol {
switch self {
case .userInfo, .userStatus, .phoneUsageToday, .phoneUsagePeriod, .phoneUsageReport, .phoneUsageTrips, .queryPhoneUsageIcon, .phoneLockApps, .phoneLocked, .phoneUnlockRequests, .phoneUnlockAllow, .signInInfo, .notice, .followList, .relations:
return .get
case .changePhone, .setGender, .changeAvater:
case .changePhone, .changeAvater:
return .put
case .deleteAccount:
return .delete

View File

@ -0,0 +1,8 @@
{
"images" : [
{ "idiom" : "universal", "scale" : "1x" },
{ "filename" : "image_broken@2x.png", "idiom" : "universal", "scale" : "2x" },
{ "filename" : "image_broken@3x.png", "idiom" : "universal", "scale" : "3x" }
],
"info" : { "author" : "xcode", "version" : 1 }
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 113 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 171 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.0 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "report_screen_tip_arrow@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "report_screen_tip_arrow@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 246 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 390 B

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "report_usage_tip_arrow@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "report_usage_tip_arrow@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 250 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 399 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 444 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 822 B

View File

@ -0,0 +1,8 @@
{
"images" : [
{ "idiom" : "universal", "scale" : "1x" },
{ "filename" : "danger@2x.png", "idiom" : "universal", "scale" : "2x" },
{ "filename" : "danger@3x.png", "idiom" : "universal", "scale" : "3x" }
],
"info" : { "author" : "xcode", "version" : 1 }
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 438 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 960 B

View File

@ -0,0 +1,9 @@
{
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"provides-namespace" : true
}
}

View File

@ -0,0 +1,8 @@
{
"images" : [
{ "idiom" : "universal", "scale" : "1x" },
{ "filename" : "app_usage@2x.png", "idiom" : "universal", "scale" : "2x" },
{ "filename" : "app_usage@3x.png", "idiom" : "universal", "scale" : "3x" }
],
"info" : { "author" : "xcode", "version" : 1 }
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

View File

@ -0,0 +1,8 @@
{
"images" : [
{ "idiom" : "universal", "scale" : "1x" },
{ "filename" : "lock_apps@2x.png", "idiom" : "universal", "scale" : "2x" },
{ "filename" : "lock_apps@3x.png", "idiom" : "universal", "scale" : "3x" }
],
"info" : { "author" : "xcode", "version" : 1 }
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

View File

@ -0,0 +1,8 @@
{
"images" : [
{ "idiom" : "universal", "scale" : "1x" },
{ "filename" : "lock_style@2x.png", "idiom" : "universal", "scale" : "2x" },
{ "filename" : "lock_style@3x.png", "idiom" : "universal", "scale" : "3x" }
],
"info" : { "author" : "xcode", "version" : 1 }
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

View File

@ -0,0 +1,8 @@
{
"images" : [
{ "idiom" : "universal", "scale" : "1x" },
{ "filename" : "phone_report@2x.png", "idiom" : "universal", "scale" : "2x" },
{ "filename" : "phone_report@3x.png", "idiom" : "universal", "scale" : "3x" }
],
"info" : { "author" : "xcode", "version" : 1 }
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

View File

@ -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 }
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

View File

@ -0,0 +1,8 @@
{
"images" : [
{ "idiom" : "universal", "scale" : "1x" },
{ "filename" : "report_status@2x.png", "idiom" : "universal", "scale" : "2x" },
{ "filename" : "report_status@3x.png", "idiom" : "universal", "scale" : "3x" }
],
"info" : { "author" : "xcode", "version" : 1 }
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

View File

@ -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 }
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

View File

@ -0,0 +1,8 @@
{
"images" : [
{ "idiom" : "universal", "scale" : "1x" },
{ "filename" : "usage_duration@2x.png", "idiom" : "universal", "scale" : "2x" },
{ "filename" : "usage_duration@3x.png", "idiom" : "universal", "scale" : "3x" }
],
"info" : { "author" : "xcode", "version" : 1 }
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

View File

@ -0,0 +1,8 @@
{
"images" : [
{ "idiom" : "universal", "scale" : "1x" },
{ "filename" : "usage_frequent@2x.png", "idiom" : "universal", "scale" : "2x" },
{ "filename" : "usage_frequent@3x.png", "idiom" : "universal", "scale" : "3x" }
],
"info" : { "author" : "xcode", "version" : 1 }
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

View File

@ -9,63 +9,63 @@ final class PrivilegeDeniedOverlay: UIView {
var onAction: (() -> Void)?
private let blurView = UIVisualEffectView(effect: UIBlurEffect(style: .light))
private let dimView = UIView()
private let imageView = UIImageView()
private let titleLab = UILabel()
private let messageLab = UILabel()
private let actionBtn = UIButton(type: .custom)
private let contentStack = UIStackView()
override init(frame: CGRect) {
super.init(frame: frame)
isHidden = true
clipsToBounds = true
blurView.isUserInteractionEnabled = false
addSubview(blurView)
blurView.layoutChain.edges()
dimView.backgroundColor = UIColor.white.withAlphaComponent(0.45)
dimView.isUserInteractionEnabled = false
addSubview(dimView)
dimView.layoutChain.edges()
imageView.contentMode = .scaleAspectFill
imageView.clipsToBounds = true
imageView.isUserInteractionEnabled = false
addSubview(imageView)
imageView.layoutChain.edges()
titleLab.font = .systemFont(ofSize: 16, weight: .semibold)
titleLab.textColor = UIColor(hexStr: "#293445")
titleLab.textAlignment = .center
addSubview(titleLab)
messageLab.font = .systemFont(ofSize: 16, weight: .semibold)
messageLab.textColor = UIColor(hexStr: "#293445")
messageLab.textAlignment = .center
addSubview(messageLab)
actionBtn.titleLabel?.font = .systemFont(ofSize: 16, weight: .semibold)
actionBtn.setTitleColor(.white, for: .normal)
actionBtn.backgroundColor = UIColor(hexStr: "#16B3FF")
actionBtn.cornerRadius = 10
actionBtn.addTarget(self, action: #selector(tapAction), for: .touchUpInside)
addSubview(actionBtn)
actionBtn.layoutChain.width(160).height(44)
titleLab.layoutChain.centerX()
titleLab.centerYAnchor.constraint(equalTo: centerYAnchor, constant: -28).isActive = true
messageLab.layoutChain
.topToBottomOfView(titleLab, offset: 6)
.centerX()
actionBtn.layoutChain
.topToBottomOfView(messageLab, offset: 16)
.centerX()
.width(160)
.height(44)
contentStack.axis = .vertical
contentStack.alignment = .center
contentStack.spacing = 6
contentStack.addArrangedSubview(titleLab)
contentStack.addArrangedSubview(messageLab)
contentStack.setCustomSpacing(16, after: messageLab)
contentStack.addArrangedSubview(actionBtn)
addSubview(contentStack)
contentStack.layoutChain.centerX().centerY()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func configure(title: String, message: String, actionTitle: String) {
func configure(title: String, message: String, actionTitle: String, imageName: String) {
imageView.image = UIImage(named: imageName)
titleLab.text = title
messageLab.text = message
actionBtn.setTitle(actionTitle, for: .normal)
let showsContent = !title.isEmpty || !message.isEmpty || !actionTitle.isEmpty
contentStack.isHidden = !showsContent
actionBtn.isHidden = actionTitle.isEmpty
titleLab.isHidden = title.isEmpty
messageLab.isHidden = message.isEmpty
}
func setVisible(_ visible: Bool) {

View File

@ -21,7 +21,7 @@ extension UIDevice {
deviceModelMap[machineIdentifier] ?? "iPhone"
}
/// `nil`
/// API `UIDevice.batteryLevel` 5% 1%
static var batteryPercent: Int? {
let device = UIDevice.current
if !device.isBatteryMonitoringEnabled {
@ -29,7 +29,7 @@ extension UIDevice {
}
let level = device.batteryLevel
guard level >= 0 else { return nil }
return Int((level * 100).rounded())
return Int((Double(level) * 100).rounded())
}
/// /

View File

@ -54,7 +54,7 @@ struct UserConfigModel: Mappable {
temp <- map["temp"]
phone <- map["phone"]
name <- map["name"]
uid <- map["user_id"]
uid <- (map["user_id"], kFlexString)
avater <- map["avater"]
sex <- map["sex"]
head_pic <- (map["head_pic"], kFlexString)

View File

@ -25,6 +25,8 @@ enum MqttType: String, Codable {
case kick = "kick" //
case signIn = "signIn" //
case sos = "sos" //
case sosQuit = "sosquit" //
case bubbleQuit = "bubblequit" // 退
case needTrack = "needtrack" //
case emote = "emote" //
case phone = "phone" //
@ -1022,9 +1024,7 @@ final class MQTTService: NSObject {
func reportLocation(lat: Double, lon: Double, addr: String,
speed: CLLocationSpeed, bearing: CLLocationDirection,
altitude: CLLocationDistance, accuracy: CLLocationAccuracy) {
let battery = UIDevice.current.batteryLevel > 0
? Int(UIDevice.current.batteryLevel * 100)
: 0
let battery = UIDevice.batteryPercent.map(String.init) ?? ""
let point = Points(
lat: lat, lon: lon, addr: addr,
@ -1032,7 +1032,7 @@ final class MQTTService: NSObject {
speed: speed, bearing: bearing,
altitude: altitude, accuracy: accuracy
)
let location = MqttLocation(battery: battery.string, points: [point])
let location = MqttLocation(battery: battery, points: [point])
guard let jsonData = try? JSONEncoder().encode(location),
let dataDict = try? JSONSerialization.jsonObject(with: jsonData) as? [String: Any]

View File

@ -7,6 +7,7 @@ import AVFoundation
import CoreTelephony
import Foundation
import Network
import NetworkExtension
import UIKit
struct PhoneStatusSnapshot: Equatable {
@ -67,7 +68,7 @@ final class PhoneStatusCollector: NSObject {
)
center.addObserver(
self,
selector: #selector(handleChange),
selector: #selector(handleApplicationDidBecomeActive),
name: UIApplication.didBecomeActiveNotification,
object: nil
)
@ -119,6 +120,11 @@ final class PhoneStatusCollector: NSObject {
emitChange()
}
@objc private func handleApplicationDidBecomeActive() {
networkMonitor.refreshWiFiName()
emitChange()
}
private func emitChange() {
let work = {
self.pendingNotify?.cancel()
@ -163,41 +169,68 @@ final class PhoneStatusCollector: NSObject {
private final class PhoneNetworkStatusMonitor {
private let monitor = NWPathMonitor()
private let monitor = Network.NWPathMonitor()
private let queue = DispatchQueue(label: "com.quicklocation.phone.network")
private let lock = NSLock()
private let telephonyInfo = CTTelephonyNetworkInfo()
private var path: NWPath?
private var path: Network.NWPath?
private var wifiName: String?
private var onChange: (() -> Void)?
func start(onChange: @escaping () -> Void) {
self.onChange = onChange
monitor.pathUpdateHandler = { [weak self] path in
self?.lock.lock()
self?.path = path
self?.lock.unlock()
guard let self else { return }
self.lock.lock()
self.path = path
self.lock.unlock()
if path.status == .satisfied,
path.usesInterfaceType(Network.NWInterface.InterfaceType.wifi) {
self.refreshWiFiName()
} else {
self.updateWiFiName(nil)
}
onChange()
}
monitor.start(queue: queue)
}
func refreshWiFiName() {
NEHotspotNetwork.fetchCurrent { [weak self] network in
self?.updateWiFiName(network?.ssid)
}
}
var statusText: String {
lock.lock()
let currentPath = path
let currentWiFiName = wifiName
lock.unlock()
guard let currentPath, currentPath.status == .satisfied else {
return "无网络"
}
if currentPath.usesInterfaceType(.wifi) {
return "Wi-Fi"
if currentPath.usesInterfaceType(Network.NWInterface.InterfaceType.wifi) {
return currentWiFiName ?? "Wi-Fi"
}
guard currentPath.usesInterfaceType(.cellular) else {
guard currentPath.usesInterfaceType(Network.NWInterface.InterfaceType.cellular) else {
return "无网络"
}
return cellularGeneration
}
private func updateWiFiName(_ name: String?) {
let normalizedName = name?.trimmingCharacters(in: .whitespacesAndNewlines)
let newName = normalizedName?.isEmpty == false ? normalizedName : nil
lock.lock()
let changed = wifiName != newName
wifiName = newName
lock.unlock()
if changed {
onChange?()
}
}
private var cellularGeneration: String {
let technologies = telephonyInfo.serviceCurrentRadioAccessTechnology
.map { Array($0.values) } ?? []

View File

@ -92,6 +92,7 @@ struct GroupUsersResponse: BaseModelProtocol {
var message: String?
var model: GroupInfoModel?
var list: [GroupMemberModel] = []
var reviewCount: Int = 0
init?(map: Map) {}
@ -100,6 +101,10 @@ struct GroupUsersResponse: BaseModelProtocol {
message <- map["message"]
model <- map["data.group"]
list <- map["data.mates"]
reviewCount <- (map["data.reviewCount"], kStrTransformInt)
if reviewCount == 0 {
reviewCount <- (map["data.review_count"], kStrTransformInt)
}
}
func isValid(for groupKey: String) -> Bool {
@ -310,11 +315,11 @@ extension UIImageView {
kf.cancelDownloadTask()
image = fallback
guard let imageURL = URL(string: url), !url.isEmpty else { return }
kf.setImage(with: imageURL, placeholder: fallback) { [weak self] result in
if case .failure = result {
self?.image = fallback
}
}
kf.setImage(
with: imageURL,
placeholder: fallback,
options: [.onFailureImage(kBrokenNetworkImage)]
)
}
}
@ -478,7 +483,7 @@ struct GroupMemberModel: Mappable, Equatable {
}
mutating func mapping(map: Map) {
user_id <- map["user_id"]
user_id <- (map["user_id"], kFlexString)
head_pic <- (map["head_pic"], kFlexString)
avater <- (map["avater"], kFlexString)
if avater.isEmpty {

View File

@ -9,6 +9,16 @@ import UIKit
import Kingfisher
import CoreMedia
nonisolated(unsafe) let kBrokenNetworkImage = UIImage(named: "Common/image_broken") ?? UIImage()
extension UIImage {
static var brokenNetworkImage: UIImage { kBrokenNetworkImage }
}
private let kNetworkImageFailureOptions: KingfisherOptionsInfo = [
.onFailureImage(kBrokenNetworkImage)
]
public protocol ImageSource {
/// URL
var imageURL: URL? { get }
@ -60,15 +70,18 @@ extension DLWrapper where Base: UIImageView {
base.image = image
return
}
// nil
base.image = placeholder
base.image = placeholder ?? .brokenNetworkImage
return
}
// ,
if imageURL.imageStr?.contains(".mp4") == true {
let source = AVAssetImageDataProvider(assetURL: imageURL, time: CMTimeMakeWithSeconds(0.0, preferredTimescale: 1))
base.kf.setImage(with: source.convertToSource(), placeholder: placeholder) { result in
base.kf.setImage(
with: source.convertToSource(),
placeholder: placeholder,
options: kNetworkImageFailureOptions
) { result in
switch result {
case let .success(imageinfo):
completionHandler?(imageinfo.image, nil)
@ -77,8 +90,11 @@ extension DLWrapper where Base: UIImageView {
}
}
} else {
// 使kf
base.kf.setImage(with: imageURL, placeholder: placeholder) { result in
base.kf.setImage(
with: imageURL,
placeholder: placeholder,
options: kNetworkImageFailureOptions
) { result in
switch result {
case let .success(imageinfo):
completionHandler?(imageinfo.image, nil)
@ -116,18 +132,23 @@ extension DLWrapper where Base: UIButton {
return
}
// nil
base.setImage(placeholder, for: state)
base.setImage(placeholder ?? .brokenNetworkImage, for: state)
return
}
// 使kf
base.kf.setImage(with: imageURL, for: state, placeholder: placeholder, completionHandler: { result in
switch result {
case let .success(imageinfo):
completionHandler?(imageinfo.image, nil)
case let .failure(error):
completionHandler?(nil, error)
base.kf.setImage(
with: imageURL,
for: state,
placeholder: placeholder,
options: kNetworkImageFailureOptions,
completionHandler: { result in
switch result {
case let .success(imageinfo):
completionHandler?(imageinfo.image, nil)
case let .failure(error):
completionHandler?(nil, error)
}
}
})
)
}
///
@ -153,18 +174,23 @@ extension DLWrapper where Base: UIButton {
return
}
// nil
base.setBackgroundImage(placeholder, for: state)
base.setBackgroundImage(placeholder ?? .brokenNetworkImage, for: state)
return
}
// 使kf
base.kf.setBackgroundImage(with: imageURL, for: state, placeholder: placeholder, completionHandler: { result in
switch result {
case let .success(imageinfo):
completionHandler?(imageinfo.image, nil)
case let .failure(error):
completionHandler?(nil, error)
base.kf.setBackgroundImage(
with: imageURL,
for: state,
placeholder: placeholder,
options: kNetworkImageFailureOptions,
completionHandler: { result in
switch result {
case let .success(imageinfo):
completionHandler?(imageinfo.image, nil)
case let .failure(error):
completionHandler?(nil, error)
}
}
})
)
}
}
@ -220,7 +246,7 @@ enum HeadPic {
case let .success(value):
image = value.image
case .failure:
image = UIImage()
image = .brokenNetworkImage
}
DispatchQueue.main.async {
completion(image)
@ -239,8 +265,7 @@ extension UIImageView {
}
if let url = HeadPic.remoteURL(for: trimmed) {
kf.cancelDownloadTask()
image = nil
kf.setImage(with: url, placeholder: nil)
kf.setImage(with: url, placeholder: nil, options: kNetworkImageFailureOptions)
} else {
kf.cancelDownloadTask()
image = HeadPic.localImage(trimmed)

View File

@ -34,6 +34,7 @@ final class FeatureIntroVC: BaseViewController {
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
rootView.reloadFeatureItems()
guard let config = AppContextManager.shared.systemConfig else { return }
bannerList = config.bannerList
rootView.cycleScrollView.isHidden = config.bannerList.filter { $0.enable == true }.count == 0
@ -56,6 +57,7 @@ final class FeatureIntroVC: BaseViewController {
case .pigeonMessage:
AppRouter.push(Route.pigeonMessage)
case .searchLocation:
guard AppContextManager.shared.systemConfig?.isIntercept != true else { return }
AppRouter.push(Route.searchLocation)
case .sos:
AppRouter.push(Route.sos)

View File

@ -37,7 +37,7 @@ final class FeatureIntroView: UIView {
private let contentView = UIView()
private let gridStack = UIStackView()
private let items: [FeatureIntroItem] = [
private static let allItems: [FeatureIntroItem] = [
FeatureIntroItem(
imageName: "Explore/sign_in_card",
accessibilityTitle: "还在吗",
@ -76,6 +76,8 @@ final class FeatureIntroView: UIView {
)
]
private var displayedItems: [FeatureIntroItem] = []
override init(frame: CGRect) {
super.init(frame: frame)
setupUI()
@ -118,25 +120,7 @@ final class FeatureIntroView: UIView {
gridStack.spacing = 13
contentView.addSubview(gridStack)
gridStack.translatesAutoresizingMaskIntoConstraints = false
for rowIndex in 0..<3 {
let rowStack = UIStackView()
rowStack.axis = .horizontal
rowStack.alignment = .fill
rowStack.distribution = .fillEqually
rowStack.spacing = 13
for columnIndex in 0..<2 {
let itemIndex = rowIndex * 2 + columnIndex
let item = items[itemIndex]
let card = FeatureIntroCardView(item: item)
card.tag = itemIndex
card.addTarget(self, action: #selector(cardTapped(_:)), for: .touchUpInside)
rowStack.addArrangedSubview(card)
card.heightAnchor.constraint(equalTo: card.widthAnchor, multiplier: item.aspectRatio).isActive = true
}
gridStack.addArrangedSubview(rowStack)
}
reloadFeatureItems()
headerBackgroundView.layoutChain
.edges(excludingEdge: .bottom)
@ -169,9 +153,47 @@ final class FeatureIntroView: UIView {
])
}
func reloadFeatureItems() {
let hideSearch = AppContextManager.shared.systemConfig?.isIntercept == true
displayedItems = Self.allItems.filter { !hideSearch || $0.action != .searchLocation }
rebuildGrid()
}
private func rebuildGrid() {
gridStack.arrangedSubviews.forEach {
gridStack.removeArrangedSubview($0)
$0.removeFromSuperview()
}
let columns = 2
var index = 0
while index < displayedItems.count {
let rowStack = UIStackView()
rowStack.axis = .horizontal
rowStack.alignment = .fill
rowStack.distribution = .fillEqually
rowStack.spacing = 13
for _ in 0..<columns {
if index < displayedItems.count {
let item = displayedItems[index]
let card = FeatureIntroCardView(item: item)
card.tag = index
card.addTarget(self, action: #selector(cardTapped(_:)), for: .touchUpInside)
rowStack.addArrangedSubview(card)
card.heightAnchor.constraint(equalTo: card.widthAnchor, multiplier: item.aspectRatio).isActive = true
} else {
rowStack.addArrangedSubview(UIView())
}
index += 1
}
gridStack.addArrangedSubview(rowStack)
}
}
@objc private func cardTapped(_ sender: FeatureIntroCardView) {
guard items.indices.contains(sender.tag) else { return }
onTapItem?(items[sender.tag].action)
guard displayedItems.indices.contains(sender.tag) else { return }
onTapItem?(displayedItems[sender.tag].action)
}
// MARK: -

View File

@ -495,9 +495,12 @@ final class GroupChatVC: BaseViewController {
}
self.viewModel.setPendingQuote(oim)
}
menu.onReport = {
ReceiveMessageReportPopView.show()
}
menu.onDismiss = { [weak self] in self?.bubbleMenu = nil }
let anchor = menuAnchorView(in: cell)
menu.show(in: rootView, anchor: anchor, canMention: !msg.isSelf)
menu.show(in: rootView, anchor: anchor, canMention: !msg.isSelf, canReport: !msg.isSelf)
}
// MARK: - Mention

View File

@ -974,6 +974,7 @@ private final class ChatPaddingLabel: UILabel {
final class ChatBubbleActionMenu: UIView {
var onMention: (() -> Void)?
var onReply: (() -> Void)?
var onReport: (() -> Void)?
var onDismiss: (() -> Void)?
private let panel = UIView()
@ -1025,7 +1026,7 @@ final class ChatBubbleActionMenu: UIView {
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
func show(in host: UIView, anchor: UIView, canMention: Bool) {
func show(in host: UIView, anchor: UIView, canMention: Bool, canReport: Bool) {
removeFromSuperview()
host.addSubview(self)
frame = host.bounds
@ -1039,6 +1040,9 @@ final class ChatBubbleActionMenu: UIView {
stack.addArrangedSubview(makeButton(title: "TA", imageName: "IM/at", action: #selector(handleMention)))
}
stack.addArrangedSubview(makeButton(title: "回复", imageName: "IM/reply", action: #selector(handleReply)))
if canReport {
stack.addArrangedSubview(makeButton(title: "举报", imageName: "IM/danger", action: #selector(handleReport)))
}
let panelW = itemWidth * CGFloat(max(stack.arrangedSubviews.count, 1))
let panelH = panelHeight
@ -1111,6 +1115,11 @@ final class ChatBubbleActionMenu: UIView {
dismiss()
}
@objc private func handleReport() {
onReport?()
dismiss()
}
@objc private func handleDismiss() { dismiss() }
func dismiss() {
@ -2086,14 +2095,25 @@ class ChatImageMsgCell: UITableViewCell {
private func loadPhoto(url: String, messageId: String) {
photoView.kf.cancelDownloadTask()
guard !url.isEmpty else { photoView.image = nil; return }
if let localImage = UIImage(contentsOfFile: url) {
photoView.image = localImage
func applyPhoto(_ image: UIImage?, broken: Bool) {
photoView.contentMode = broken ? .scaleAspectFit : .scaleAspectFill
photoView.image = broken ? .brokenNetworkImage : image
}
guard !url.isEmpty else {
applyPhoto(nil, broken: true)
return
}
if !url.hasPrefix("http"), !url.hasPrefix("https") {
if let localImage = UIImage(contentsOfFile: url) {
applyPhoto(localImage, broken: false)
} else {
applyPhoto(nil, broken: true)
}
return
}
photoView.dl.setImage(with: url) { [weak self] image, _ in
guard let self, self.configuredId == messageId, let image else { return }
self.photoView.image = image
guard let self, self.configuredId == messageId else { return }
applyPhoto(image, broken: image == nil)
}
}

View File

@ -47,7 +47,7 @@ final class GroupItineraryView: UIView {
addSubview(createBtn)
headerRow.layoutChain
.top(8)
.top(20)
.edgesHorzontal(16)
.height(28)
@ -223,7 +223,7 @@ extension GroupItineraryView: UITableViewDataSource, UITableViewDelegate {
))
if !sectionData.dateText.isEmpty {
attr.append(NSAttributedString(
string: " \(sectionData.dateText)",
string: " \(sectionData.dateText)",
attributes: [
.font: UIFont.systemFont(ofSize: 13, weight: .medium),
.foregroundColor: color
@ -322,13 +322,14 @@ final class GroupItineraryCell: UITableViewCell {
endLocationLab.translatesAutoresizingMaskIntoConstraints = false
tagScrollView.translatesAutoresizingMaskIntoConstraints = false
tagStackView.translatesAutoresizingMaskIntoConstraints = false
startMarkerLab.layoutChain
.topToBottomOfView(avatarImg, offset: 10)
.leftToView(nameLab)
.width(20)
.heightToWidth(1)
NSLayoutConstraint.activate([
startMarkerLab.leadingAnchor.constraint(equalTo: cardView.leadingAnchor, constant: 14),
startMarkerLab.topAnchor.constraint(equalTo: avatarImg.bottomAnchor, constant: 10),
startMarkerLab.widthAnchor.constraint(equalToConstant: 20),
startMarkerLab.heightAnchor.constraint(equalToConstant: 20),
startLocationLab.leadingAnchor.constraint(equalTo: startMarkerLab.trailingAnchor, constant: 7),
startLocationLab.centerYAnchor.constraint(equalTo: startMarkerLab.centerYAnchor),
startLocationLab.widthAnchor.constraint(lessThanOrEqualToConstant: 82),

View File

@ -218,6 +218,8 @@ final class GroupMemberDirectoryCell: UITableViewCell {
view.backgroundColor = UIColor(hexStr: "#F1F2F4")
view.contentMode = .scaleAspectFill
view.layer.cornerRadius = 10
view.layer.borderWidth = 2
view.layer.borderColor = UIColor.white.cgColor
view.clipsToBounds = true
return view
}()
@ -338,6 +340,8 @@ final class GroupMemberListCell: UICollectionViewCell {
view.backgroundColor = UIColor(hexStr: "#F1F2F4")
view.contentMode = .scaleAspectFill
view.layer.cornerRadius = 25
view.layer.borderWidth = 2
view.layer.borderColor = UIColor.white.cgColor
view.clipsToBounds = true
return view
}()

View File

@ -23,6 +23,8 @@ class GroupSettingVC: BaseViewController {
private var viewModel: GroupSettingViewModel
private var hasAppeared = false
override func viewDidLoad() {
super.viewDidLoad()
@ -30,6 +32,14 @@ class GroupSettingVC: BaseViewController {
reactiveAction()
requestGroupInfo()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if hasAppeared {
requestReviewBadge()
}
hasAppeared = true
}
// MARK: - Actions
private func reactiveAction() {
@ -107,6 +117,11 @@ class GroupSettingVC: BaseViewController {
rootView.clearHistoryView.rx.tapGesture.subscribe(onNext: { [weak self] _ in
self?.confirmClearHistory()
}).disposed(by: disposeBag)
rootView.reportGroupView.rx.tapGesture.subscribe(onNext: { [weak self] _ in
guard self?.viewModel.groupModel?.is_owner == false else { return }
ReceiveMessageReportPopView.show()
}).disposed(by: disposeBag)
//
rootView.dismissGroupView.rx.tapGesture.subscribe(onNext: { _ in
@ -127,6 +142,18 @@ class GroupSettingVC: BaseViewController {
}).disposed(by: disposeBag)
}
private func requestReviewBadge() {
guard viewModel.groupModel?.is_owner == true else {
rootView.setAuditBadgeVisible(false)
return
}
GroupService.reviewlist(requestData: ["group_key": viewModel.groupId])
.subscribe(onNext: { [weak self] response in
self?.rootView.setAuditBadgeVisible(!response.list.isEmpty)
}, onError: { _ in })
.disposed(by: disposeBag)
}
// MARK: - API
private func requestGroupInfo() {
DLToast.showLoading()
@ -144,7 +171,9 @@ class GroupSettingVC: BaseViewController {
self.rootView.setupData(model,
members: response.list,
groupNickname: self.viewModel.displayGroupNickname)
self.rootView.setAuditBadgeVisible(model.is_owner && response.reviewCount > 0)
self.requestGroupNickname()
self.requestReviewBadge()
}, onError: { error in
DLToast.dismiss()
DLToast.showError(text: error.localizedDescription)

View File

@ -27,8 +27,12 @@ final class GroupSettingView: UIView {
switchBtn.isOn = model.review
auditSwitchView.isHidden = !model.is_owner
auditMemberView.isHidden = !model.is_owner
if !model.is_owner {
auditMemberView.setShowsBadge(false)
}
dismissGroupView.isHidden = !model.is_owner
leaveGroupView.isHidden = model.is_owner
reportGroupView.isHidden = model.is_owner
groupNameView.setShowsArrow(model.is_owner)
groupTypeView.setShowsArrow(false)
@ -47,6 +51,10 @@ final class GroupSettingView: UIView {
groupNicknameView.setValue(nickname)
}
func setAuditBadgeVisible(_ visible: Bool) {
auditMemberView.setShowsBadge(visible)
}
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = UIColor(hexStr: "#F7F8FA")
@ -212,6 +220,7 @@ final class GroupSettingView: UIView {
dangerStackView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate(dangerStackView.edgesToSuperview())
dangerStackView.addArrangedSubview(dismissGroupView)
dangerStackView.addArrangedSubview(reportGroupView)
dangerStackView.addArrangedSubview(leaveGroupView)
}
@ -296,7 +305,7 @@ final class GroupSettingView: UIView {
private let memberSeparatorView = HorizontalDashLineView()
private let memberActionsView = UIView()
lazy var inviteCodeButton: UIButton = makeMemberActionButton(title: "邀请", imageName: "GroupSetting/invite_code")
lazy var inviteCodeButton: UIButton = makeMemberActionButton(title: "邀请成员", imageName: "GroupSetting/invite_code")
lazy var shareButton: UIButton = makeMemberActionButton(title: "分享", imageName: "GroupSetting/share")
private lazy var actionDividerView: UIView = {
@ -343,6 +352,7 @@ final class GroupSettingView: UIView {
}()
let dismissGroupView = GroupSettingActionRow(title: "解散圈子", titleColor: UIColor(hexStr: "#FF4D53"), hidesDivider: true)
let reportGroupView = GroupSettingActionRow(title: "举报", titleColor: UIColor(hexStr: "#FF4D53"))
let leaveGroupView = GroupSettingActionRow(title: "退出圈子", titleColor: UIColor(hexStr: "#FF4D53"), hidesDivider: true)
private func makeCardView() -> UIView {
@ -558,6 +568,7 @@ final class GroupSettingInfoRow: UIView {
private let style: Style
private let titleLab = UILabel()
private let badgeView = UIView()
private let valueLab = UILabel()
private let valueImageView = UIImageView()
private let arrowImageView = UIImageView(image: UIImage(named: "GroupSetting/arrow_right"))
@ -596,6 +607,10 @@ final class GroupSettingInfoRow: UIView {
imageTrailingToRow?.isActive = !showsArrow
}
func setShowsBadge(_ showsBadge: Bool) {
badgeView.isHidden = !showsBadge
}
private func setupUI() {
backgroundColor = .clear
titleLab.textColor = UIColor(hexStr: "#293445")
@ -613,14 +628,21 @@ final class GroupSettingInfoRow: UIView {
arrowImageView.contentMode = .scaleAspectFit
dividerView.backgroundColor = UIColor(hexStr: "#EDEEF0")
switchButton.onTintColor = UIColor(hexStr: "#16B3FF")
badgeView.backgroundColor = UIColor(hexStr: "#FF5260")
badgeView.layer.cornerRadius = 4
badgeView.isHidden = true
[titleLab, valueLab, valueImageView, arrowImageView, switchButton, dividerView].forEach {
[titleLab, badgeView, valueLab, valueImageView, arrowImageView, switchButton, dividerView].forEach {
addSubview($0)
$0.translatesAutoresizingMaskIntoConstraints = false
}
NSLayoutConstraint.activate([
titleLab.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16),
badgeView.trailingAnchor.constraint(equalTo: arrowImageView.leadingAnchor, constant: -4),
badgeView.centerYAnchor.constraint(equalTo: arrowImageView.centerYAnchor),
badgeView.widthAnchor.constraint(equalToConstant: 8),
badgeView.heightAnchor.constraint(equalToConstant: 8),
arrowImageView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -16),
arrowImageView.centerYAnchor.constraint(equalTo: centerYAnchor),
arrowImageView.widthAnchor.constraint(equalToConstant: 16),
@ -733,8 +755,8 @@ final class GroupSettingTagRow: UIView {
$0.removeFromSuperview()
}
if tags.count > 2 {
tagStack.addArrangedSubview(GroupSettingTagChip(text: "···", compact: true))
tags.suffix(2).forEach { tagStack.addArrangedSubview(GroupSettingTagChip(text: $0, compact: false)) }
tagStack.addArrangedSubview(GroupSettingTagChip(text: "···", compact: true))
} else {
tags.forEach { tagStack.addArrangedSubview(GroupSettingTagChip(text: $0, compact: false)) }
}

View File

@ -24,6 +24,13 @@ class GroupView: UIView {
var disposeBag = DisposeBag()
private let actionCardSpacing: CGFloat = 13
private let listRowHeight: CGFloat = 65
private let listBottomSpacing: CGFloat = 15
private let tabBarContentHeight: CGFloat = 49
private var listHeightConstraint: NSLayoutConstraint?
private var joinedItemCount = 0
private var createdItemCount = 0
private var lastLayoutSize: CGSize = .zero
private var actionCardHeight: CGFloat {
let availableWidth = UIScreen.main.bounds.width - 32 - actionCardSpacing
@ -63,7 +70,8 @@ class GroupView: UIView {
segmentView.addSubview(joinedTabLabel)
segmentView.addSubview(createdTabLabel)
segmentView.addSubview(tabIndicator)
listContainer.addSubview(segmentScrollView)
listContainer.addSubview(listCardView)
listCardView.addSubview(segmentScrollView)
segmentScrollView.addSubview(segmentContentView)
// (joined) | (created)
segmentContentView.addSubview(joinedTableView)
@ -90,7 +98,7 @@ class GroupView: UIView {
circleTabLabel.layoutChain
.left(20)
.bottom(10)
.bottom()
.height(28)
itineraryTabLabel.layoutChain
@ -160,25 +168,30 @@ class GroupView: UIView {
.centerX(joinedTabLabel)
.width(19).height(4)
segmentScrollView.layoutChain
.topToBottomOfView(segmentView)
.edgesHorzontal()
listCardView.layoutChain
.topToBottomOfView(segmentView, offset: 6)
.edgesHorzontal(15)
.bottom()
.height(kScreenHeight - kNaviHeight - 44 - 12 - actionCardHeight - 16)
listHeightConstraint = listCardView.heightAnchor.constraint(equalToConstant: listRowHeight)
listHeightConstraint?.isActive = true
segmentScrollView.layoutChain
.edges()
segmentContentView.layoutChain
.edges()
.heightToView(segmentScrollView)
.widthToView(segmentScrollView, multiplier: 2)
let svWidth = UIScreen.main.bounds.width
joinedTableView.layoutChain
.top().bottom().left()
.width(svWidth)
.widthToView(segmentScrollView)
createdTableView.layoutChain
.top().bottom()
.leftToRightOfView(joinedTableView)
.width(svWidth)
.right()
.widthToView(segmentScrollView)
//
cycleScrollView.layoutChain
@ -192,10 +205,9 @@ class GroupView: UIView {
override func layoutSubviews() {
super.layoutSubviews()
segmentContentView.layoutChain.width(bounds.width * 2)
listContainer.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
listContainer.layer.cornerRadius = 20
listContainer.clipsToBounds = true
guard bounds.size != lastLayoutSize else { return }
lastLayoutSize = bounds.size
updateListHeight(animated: false)
}
// MARK: - Nav
@ -350,15 +362,23 @@ class GroupView: UIView {
// MARK: - List container
private lazy var listContainer: UIView = {
let v = UIView()
v.backgroundColor = .clear
return v
}()
private lazy var listCardView: UIView = {
let v = UIView()
v.backgroundColor = .white
v.layer.cornerRadius = 10
v.clipsToBounds = true
return v
}()
// MARK: - Segment0 =joined1 =created
private(set) lazy var segmentView: UIView = {
let v = UIView()
v.backgroundColor = .white
v.backgroundColor = .clear
let line = UIView()
line.backgroundColor = UIColor(hexStr: "#B5B5B5").withAlphaComponent(0.12)
@ -375,7 +395,7 @@ class GroupView: UIView {
private(set) lazy var joinedTabLabel: UILabel = {
let label = UILabel()
label.text = "全部圈子"
label.font = .systemFont(ofSize: 16, weight: .heavy)
label.font = .systemFont(ofSize: 16, weight: .bold)
label.textColor = UIColor(hexStr: "#2A3648")
label.textAlignment = .center
label.isUserInteractionEnabled = true
@ -387,7 +407,7 @@ class GroupView: UIView {
let label = UILabel()
label.text = "我的圈子"
label.font = .systemFont(ofSize: 16, weight: .medium)
label.textColor = UIColor(hexStr: "#2A3648")
label.textColor = UIColor(hexStr: "#2A3648", alpha: 0.5)
label.textAlignment = .center
label.isUserInteractionEnabled = true
return label
@ -424,9 +444,9 @@ class GroupView: UIView {
tv.showsVerticalScrollIndicator = false
tv.register(CircleGroupCell.self)
tv.estimatedRowHeight = 70
tv.rowHeight = 70
tv.bounces = true
tv.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 97 + kSafeBottomMargin, right: 0)
tv.rowHeight = listRowHeight
tv.bounces = false
tv.isScrollEnabled = false
return tv
}()
@ -437,9 +457,9 @@ class GroupView: UIView {
tv.showsVerticalScrollIndicator = false
tv.register(CircleGroupCell.self)
tv.estimatedRowHeight = 70
tv.rowHeight = 70
tv.bounces = true
tv.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 97 + kSafeBottomMargin, right: 0)
tv.rowHeight = listRowHeight
tv.bounces = false
tv.isScrollEnabled = false
return tv
}()
@ -450,12 +470,13 @@ class GroupView: UIView {
func selectSegment(at index: Int) {
currentSegmentIndex = index
let isAll = index == 0
updateListHeight(animated: false)
UIView.animate(withDuration: 0.2) {
self.joinedTabLabel.textColor = isAll ? UIColor(hexStr: "#2A3648") : UIColor(hexStr: "#2A3648", alpha: 0.5)
self.joinedTabLabel.font = .systemFont(ofSize: 16, weight: isAll ? .heavy : .medium)
self.joinedTabLabel.font = .systemFont(ofSize: 16, weight: isAll ? .bold : .medium)
self.createdTabLabel.textColor = isAll ? UIColor(hexStr: "#2A3648", alpha: 0.5) : UIColor(hexStr: "#2A3648")
self.createdTabLabel.font = .systemFont(ofSize: 16, weight: isAll ? .medium : .heavy)
self.createdTabLabel.font = .systemFont(ofSize: 16, weight: isAll ? .medium : .bold)
self.tabIndicator.center.x = isAll
? self.joinedTabLabel.center.x
@ -465,6 +486,65 @@ class GroupView: UIView {
}
}
func updateListCount(_ itemCount: Int, forSegment index: Int) {
if index == 0 {
joinedItemCount = itemCount
} else {
createdItemCount = itemCount
}
guard index == currentSegmentIndex else {
updateTableScrolling()
return
}
updateListHeight(animated: true)
}
private func updateListHeight(animated: Bool) {
guard bounds.height > 0 else { return }
if animated {
layoutIfNeeded()
}
let itemCount = currentSegmentIndex == 0 ? joinedItemCount : createdItemCount
let contentHeight = CGFloat(max(itemCount, 1)) * listRowHeight
let safeBottom = max(kSafeBottomMargin, safeAreaInsets.bottom)
let tabBarTop = bounds.height - tabBarContentHeight - safeBottom
let cardTop = listCardView.convert(.zero, to: self).y
let maxHeight = max(0, tabBarTop - listBottomSpacing - cardTop)
let targetHeight = min(contentHeight, maxHeight)
guard abs((listHeightConstraint?.constant ?? 0) - targetHeight) > 0.5 else {
updateTableScrolling(maxHeight: maxHeight)
return
}
listHeightConstraint?.constant = targetHeight
updateTableScrolling(maxHeight: maxHeight)
guard animated else { return }
UIView.animate(withDuration: 0.2) {
self.layoutIfNeeded()
}
}
private func updateTableScrolling(maxHeight: CGFloat? = nil) {
let availableHeight: CGFloat
if let maxHeight {
availableHeight = maxHeight
} else {
let safeBottom = max(kSafeBottomMargin, safeAreaInsets.bottom)
let tabBarTop = bounds.height - tabBarContentHeight - safeBottom
let cardTop = listCardView.convert(.zero, to: self).y
availableHeight = max(0, tabBarTop - listBottomSpacing - cardTop)
}
let joinedScrollable = CGFloat(joinedItemCount) * listRowHeight > availableHeight + 0.5
let createdScrollable = CGFloat(createdItemCount) * listRowHeight > availableHeight + 0.5
joinedTableView.isScrollEnabled = joinedScrollable
joinedTableView.bounces = joinedScrollable
createdTableView.isScrollEnabled = createdScrollable
createdTableView.bounces = createdScrollable
}
func selectTopTab(at index: Int) {
guard index == 0 || index == 1 else { return }
currentTopTab = index
@ -511,8 +591,16 @@ extension GroupView: UIScrollViewDelegate {
}
}
///
private var isMainScrollable: Bool {
let inset = mainScrollView.adjustedContentInset
return mainScrollView.contentSize.height + inset.top + inset.bottom
> mainScrollView.bounds.height + 0.5
}
/// GroupViewController rx.didScroll
func handleTableViewScroll(_ scrollView: UIScrollView) {
guard isMainScrollable else { return }
if isSubCanScroll {
if scrollView.contentOffset.y <= 0 {
isSubCanScroll = false
@ -526,6 +614,7 @@ extension GroupView: UIScrollViewDelegate {
// MARK: - scroll
private func handleMainScroll(_ scrollView: UIScrollView) {
guard isMainScrollable else { return }
if isSubCanScroll {
scrollView.contentOffset.y = stickThreshold
isSticky = true
@ -673,6 +762,12 @@ final class CircleGroupCell: UITableViewCell {
return view
}()
private let separatorView: UIView = {
let view = UIView()
view.backgroundColor = UIColor(hexStr: "#EEEEEE")
return view
}()
override init(style: CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
selectionStyle = .none
@ -687,6 +782,7 @@ final class CircleGroupCell: UITableViewCell {
contentView.addSubview(timeLabel)
contentView.addSubview(previewLabel)
contentView.addSubview(tagStack)
contentView.addSubview(separatorView)
iconView.layoutChain
.left(15)
@ -720,6 +816,14 @@ final class CircleGroupCell: UITableViewCell {
.topToBottomOfView(nameLabel, offset: 4)
.leftToView(nameLabel)
.rightToLeftOfView(tagStack, offset: -8)
separatorView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
separatorView.leadingAnchor.constraint(equalTo: nameLabel.leadingAnchor),
separatorView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
separatorView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
separatorView.heightAnchor.constraint(equalToConstant: 1.0 / UIScreen.main.scale)
])
}
required init?(coder: NSCoder) {
@ -731,8 +835,9 @@ final class CircleGroupCell: UITableViewCell {
iconView.setGroupIcon(url: "", fallback: nil)
}
func configure(_ model: GroupCellData) {
func configure(_ model: GroupCellData, showsSeparator: Bool) {
let g = model.group
separatorView.isHidden = !showsSeparator
iconView.setGroupIcon(url: model.iconURL, fallback: model.localIcon)
nameLabel.text = "\(g.name) (\(g.people_no))"
previewLabel.text = model.lastMsgPreview

View File

@ -116,11 +116,19 @@ final class GroupViewController: BaseViewController {
viewModel.output.createdSections
.observe(on: MainScheduler.asyncInstance)
.do(onNext: { [weak self] sections in
let itemCount = sections.reduce(0) { $0 + $1.items.count }
self?.rootView.updateListCount(itemCount, forSegment: 1)
})
.bind(to: rootView.createdTableView.rx.items(dataSource: createdDataSource))
.disposed(by: disposeBag)
viewModel.output.joinedSections
.observe(on: MainScheduler.asyncInstance)
.do(onNext: { [weak self] sections in
let itemCount = sections.reduce(0) { $0 + $1.items.count }
self?.rootView.updateListCount(itemCount, forSegment: 0)
})
.bind(to: rootView.joinedTableView.rx.items(dataSource: joinedDataSource))
.disposed(by: disposeBag)
@ -354,17 +362,21 @@ final class GroupViewController: BaseViewController {
}()
private lazy var createdDataSource: RxTableViewSectionedReloadDataSource<CircleListSectionModel> = {
RxTableViewSectionedReloadDataSource<CircleListSectionModel> { _, tableView, indexPath, model in
RxTableViewSectionedReloadDataSource<CircleListSectionModel> { dataSource, tableView, indexPath, model in
let cell: CircleGroupCell = tableView.dequeueReusableCell(for: indexPath)
cell.configure(model)
let isLastRow = indexPath.section == dataSource.sectionModels.count - 1
&& indexPath.row == dataSource.sectionModels[indexPath.section].items.count - 1
cell.configure(model, showsSeparator: !isLastRow)
return cell
}
}()
private lazy var joinedDataSource: RxTableViewSectionedReloadDataSource<CircleListSectionModel> = {
RxTableViewSectionedReloadDataSource<CircleListSectionModel> { _, tableView, indexPath, model in
RxTableViewSectionedReloadDataSource<CircleListSectionModel> { dataSource, tableView, indexPath, model in
let cell: CircleGroupCell = tableView.dequeueReusableCell(for: indexPath)
cell.configure(model)
let isLastRow = indexPath.section == dataSource.sectionModels.count - 1
&& indexPath.row == dataSource.sectionModels[indexPath.section].items.count - 1
cell.configure(model, showsSeparator: !isLastRow)
return cell
}
}()

View File

@ -85,7 +85,7 @@ class InviteJoinView: UIView {
inviteCardView.layoutChain
.topToBottomOfView(navView, offset: 42)
.edgesHorzontal(15)
.heightToWidth(481/345)
.heightToWidth(518/345)
cardBgImg.layoutChain.edges()
@ -116,10 +116,10 @@ class InviteJoinView: UIView {
.centerX()
qrCodeView.layoutChain
.bottom(42)
.bottom(38)
.centerX()
.width(138)
.height(138)
.width(180)
.height(180)
qrCodeImgView.layoutChain
.edges(all: 18)

View File

@ -121,8 +121,13 @@ final class MailboxView: UIView {
headerGradientLayer.locations = [0, 0.58, 1]
headerGradientLayer.startPoint = CGPoint(x: 0, y: 0)
headerGradientLayer.endPoint = CGPoint(x: 1, y: 1)
layer.insertSublayer(headerGradientLayer, at: 0)
// layer.insertSublayer(headerGradientLayer, at: 0)
addSubview(navBgView)
navBgView.layoutChain
.edges(excludingEdge: .bottom)
.heightToWidth(160/375)
setupNavigationBar()
setupCategories()
setupTableView()
@ -252,6 +257,13 @@ final class MailboxView: UIView {
loadingIndicator.centerYAnchor.constraint(equalTo: emptyView.centerYAnchor, constant: -40)
])
}
lazy var navBgView: UIImageView = {
let iv = UIImageView()
iv.image = UIImage(named: "Common/navBar_bg_1")
iv.contentMode = .scaleAspectFill
return iv
}()
@objc private func categoryTapped(_ sender: MailboxCategoryView) {
onSelectCategory?(sender.category)

View File

@ -71,6 +71,31 @@ final class AppUsageReportVC: BaseViewController {
self?.rootView.apply(snapshot)
})
.disposed(by: disposeBag)
viewModel.selectedMemberId
.observe(on: MainScheduler.instance)
.subscribe(onNext: { [weak self] _ in
self?.updatePrivilegeOverlay()
})
.disposed(by: disposeBag)
rootView.onRemind = { [weak self] in
guard let self else { return }
GroupService.sendPrivilegeRemind(
content: "请开启App使用记录隐私设置",
groupKey: self.viewModel.groupKey,
userId: self.viewModel.selectedMemberId.value
)
}
updatePrivilegeOverlay()
}
private func updatePrivilegeOverlay() {
let userId = viewModel.selectedMemberId.value
let isSelf = userId == AppContextManager.shared.userId
let member = viewModel.members.first { $0.user_id == userId }
let denied = !isSelf && member?.extra.privilege.app_usage_record == false
rootView.setPrivilegeDenied(denied)
}
}

View File

@ -17,13 +17,14 @@ final class AppUsageReportView: UIView {
let view = UICollectionView(frame: .zero, collectionViewLayout: layout)
view.backgroundColor = .white
view.showsHorizontalScrollIndicator = false
view.contentInset = UIEdgeInsets(top: 0, left: 12, bottom: 0, right: 12)
view.contentInset = UIEdgeInsets(top: 3, left: 6, bottom: 0, right: 6)
view.register(GroupMemberListCell.self)
view.applyAppUsageReportCardStyle(cornerRadius: 20)
return view
}()
var onPeriodChanged: ((AppUsageReportPeriod) -> Void)?
var onRemind: (() -> Void)?
private let navBgView = UIImageView(image: UIImage(named: "Common/navBar_bg_2"))
private let scrollView = UIScrollView()
@ -52,6 +53,12 @@ final class AppUsageReportView: UIView {
)
}
func setPrivilegeDenied(_ denied: Bool) {
let remind: () -> Void = { [weak self] in self?.onRemind?() }
frequentSection.setDenied(denied, imageName: "PrivilegeDenied/usage_frequent", onRemind: remind)
durationSection.setDenied(denied, imageName: "PrivilegeDenied/usage_duration", onRemind: remind)
}
private func bindPeriodControls() {
let onChange: (AppUsageReportPeriod) -> Void = { [weak self] period in
self?.onPeriodChanged?(period)
@ -142,6 +149,7 @@ private final class AppUsageFrequentSectionView: UIView {
private let periodControl = AppUsagePeriodControl()
private let rowsStack = UIStackView()
private let statusLab = UILabel()
private let deniedOverlay = PrivilegeDeniedOverlay()
override init(frame: CGRect) {
super.init(frame: frame)
@ -214,6 +222,22 @@ private final class AppUsageFrequentSectionView: UIView {
statusLab.numberOfLines = 0
cardView.addSubview(statusLab)
statusLab.layoutChain.centerX().centerY(cardView, offset: 16).left(24).right(24)
cardView.addSubview(deniedOverlay)
deniedOverlay.layoutChain.edges()
deniedOverlay.layer.cornerRadius = 20
}
func setDenied(_ denied: Bool, imageName: String, onRemind: (() -> Void)?) {
deniedOverlay.configure(
title: "TA未开启设置",
message: "无法查看手机状态",
actionTitle: "提醒TA",
imageName: imageName
)
deniedOverlay.onAction = onRemind
deniedOverlay.setVisible(denied)
if denied { cardView.bringSubviewToFront(deniedOverlay) }
}
}
@ -232,6 +256,7 @@ private final class AppUsageDurationSectionView: UIView {
private let totalHintLab = UILabel()
private let legendStack = UIStackView()
private let statusLab = UILabel()
private let deniedOverlay = PrivilegeDeniedOverlay()
override init(frame: CGRect) {
super.init(frame: frame)
@ -336,6 +361,22 @@ private final class AppUsageDurationSectionView: UIView {
statusLab.numberOfLines = 0
cardView.addSubview(statusLab)
statusLab.layoutChain.centerX().centerY(cardView, offset: 18).left(24).right(24)
cardView.addSubview(deniedOverlay)
deniedOverlay.layoutChain.edges()
deniedOverlay.layer.cornerRadius = 20
}
func setDenied(_ denied: Bool, imageName: String, onRemind: (() -> Void)?) {
deniedOverlay.configure(
title: "TA未开启设置",
message: "无法查看手机状态",
actionTitle: "提醒TA",
imageName: imageName
)
deniedOverlay.onAction = onRemind
deniedOverlay.setVisible(denied)
if denied { cardView.bringSubviewToFront(deniedOverlay) }
}
}

View File

@ -61,7 +61,7 @@ final class AppUsageReportViewModel {
let selectedPeriod = BehaviorRelay<AppUsageReportPeriod>(value: .week)
let snapshot = BehaviorRelay<Snapshot>(value: .init(state: .loading, period: .week, totalUsageTime: 0, apps: []))
private let groupKey: String
let groupKey: String
private let disposeBag = DisposeBag()
private var reportCache: [String: PhoneUsagePeriodModel] = [:]
private var requestIDs: [String: UUID] = [:]

View File

@ -172,7 +172,7 @@ class GroupListPopView: UIView {
private func updateTableHeight() {
let visible = min(groupList.count, Self.maxVisibleRows)
let height = CGFloat(visible) * Self.rowHeight
tableView.layoutChain.height(height)
tableView.layoutChain.height(groupList.count > 3 ? height + 30 : height)
tableView.isScrollEnabled = groupList.count > Self.maxVisibleRows
tableView.reloadData()
}
@ -315,11 +315,8 @@ class GroupListCell: UITableViewCell {
cardView.layer.borderColor = GroupListPopView.primaryBlueColor.cgColor
checkIcon.image = isSelected
? UIImage(systemName: "checkmark.circle.fill")
: UIImage(systemName: "circle")
checkIcon.tintColor = isSelected
? GroupListPopView.primaryBlueColor
: UIColor(hexStr: "#D0D0D0")
? UIImage(named: "Common/checkbox_on")
: UIImage(named: "Common/checkbox")
}
private static func makeTagLabel(_ text: String) -> UILabel {
@ -365,8 +362,8 @@ class GroupListCell: UITableViewCell {
checkIcon.layoutChain
.right(12)
.centerY()
.width(22)
.height(22)
.width(24)
.height(24)
nameLab.layoutChain
.topToView(avaterImgView, offset: 2)
@ -379,7 +376,7 @@ class GroupListCell: UITableViewCell {
.rightToLeftOfView(checkIcon, offset: -8, relation: .lessThanOrEqual)
countLab.layoutChain
.topToBottomOfView(nameLab, offset: 4)
.bottomToView(avaterImgView)
.leftToView(nameLab)
.rightToLeftOfView(checkIcon, offset: -8)
}
@ -437,7 +434,7 @@ class GroupListCell: UITableViewCell {
lazy var checkIcon: UIImageView = {
let view = UIImageView()
view.contentMode = .scaleAspectFit
view.contentMode = .scaleAspectFill
view.preferredSymbolConfiguration = UIImage.SymbolConfiguration(pointSize: 20, weight: .regular)
return view
}()

View File

@ -22,8 +22,13 @@ class GroupMemberView2: UIView {
private var messageEchoByUserId: [String: String] = [:]
private var phoneStatusObserver: NSObjectProtocol?
private var weatherRequestID = UUID()
private var localWeatherSnapshot: MemberWeatherSnapshot?
private var localWeatherSnapshotKey: String?
private var localWeatherUpdatedAt: Date?
private var pendingWeatherLocationKey: String?
var onCoupleMemberTap: ((GroupMemberModel) -> Void)?
private static let weatherRefreshInterval: TimeInterval = 15 * 60
private static let batteryNormalColor = UIColor(hexStr: "#47DD00")
private static let batteryLowColor = UIColor(hexStr: "#F85A5D")
private static let batteryDefaultTextColor = UIColor(hexStr: "#353B4F")
@ -45,10 +50,11 @@ class GroupMemberView2: UIView {
ownView.isHidden = !isOwner
relationIconView.configure(relationIdx: model.extra.relation_idx)
updateMoodBadge(model.mood)
statusDotView.backgroundColor = UIColor(hexStr: model.is_online ? "#67EA76" : "#D8D8D8")
statusLab.text = model.is_online ? "在线" : "离线"
let isCurrentUser = model.user_id == AppContextManager.shared.userId
let isCurrentUser = Self.isCurrentUser(model.user_id)
let isOnline = isCurrentUser || model.is_online
statusDotView.backgroundColor = UIColor(hexStr: isOnline ? "#67EA76" : "#D8D8D8")
statusLab.text = isOnline ? "在线" : "离线"
if !model.is_online, !isCurrentUser {
locationLab.text = "**"
distanceView.isHidden = true
@ -59,6 +65,8 @@ class GroupMemberView2: UIView {
applyPhoneUsage(phoneUsage)
if isCurrentUser {
applyLocalDeviceInfo()
startLocalPhoneStatusObserving()
requestLocalWeather()
}
updateRealtimeInteractionVisibility(isCurrentUser: isCurrentUser)
@ -84,6 +92,14 @@ class GroupMemberView2: UIView {
func updatePhoneUsage(_ phoneUsage: PhoneUsageTodayModel?, for userId: String) {
guard currentMemberModel?.user_id == userId else { return }
applyPhoneUsage(phoneUsage)
if Self.isCurrentUser(userId) {
applyLocalDeviceInfo()
}
}
func refreshLocalDeviceInfoIfNeeded() {
guard isShowingCurrentUser else { return }
applyLocalDeviceInfo()
}
func applyEmojiEcho(name: String, for userId: String) {
@ -223,8 +239,13 @@ class GroupMemberView2: UIView {
}
private var isShowingCurrentUser: Bool {
guard let userId = currentMemberModel?.user_id, !userId.isEmpty else { return false }
return userId == AppContextManager.shared.userId
Self.isCurrentUser(currentMemberModel?.user_id)
}
private static func isCurrentUser(_ userId: String?) -> Bool {
let memberId = userId?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
let selfId = AppContextManager.shared.userId.trimmingCharacters(in: .whitespacesAndNewlines)
return !memberId.isEmpty && memberId == selfId
}
private var isShowingOfflineOther: Bool {
@ -270,6 +291,7 @@ class GroupMemberView2: UIView {
self.phoneStatusObserver = nil
}
weatherRequestID = UUID()
pendingWeatherLocationKey = nil
}
private func applyLocalDeviceInfo() {
@ -287,31 +309,65 @@ class GroupMemberView2: UIView {
}
private func requestLocalWeather() {
let requestID = UUID()
weatherRequestID = requestID
applyUnknownWeather(text: "***")
guard let latitude = Defaults[\.currentLatitude],
let longitude = Defaults[\.currentLongitude] else {
applyUnknownWeather(text: "***")
if localWeatherSnapshot == nil {
applyUnknownWeather(text: "***")
}
return
}
let location = CLLocation(latitude: latitude, longitude: longitude)
guard CLLocationCoordinate2DIsValid(location.coordinate) else {
applyUnknownWeather(text: "***")
if localWeatherSnapshot == nil {
applyUnknownWeather(text: "***")
}
return
}
let locationKey = Self.weatherLocationKey(location.coordinate)
if let snapshot = localWeatherSnapshot {
applyWeather(snapshot)
if localWeatherSnapshotKey == locationKey,
let localWeatherUpdatedAt,
Date().timeIntervalSince(localWeatherUpdatedAt) < Self.weatherRefreshInterval {
return
}
} else {
applyUnknownWeather(text: "***")
}
guard pendingWeatherLocationKey != locationKey else { return }
let requestID = UUID()
weatherRequestID = requestID
pendingWeatherLocationKey = locationKey
MemberWeatherService.shared.weather(for: location) { [weak self] result in
guard let self, self.weatherRequestID == requestID, self.isShowingCurrentUser else { return }
guard let self, self.weatherRequestID == requestID else { return }
self.pendingWeatherLocationKey = nil
guard self.isShowingCurrentUser else { return }
switch result {
case .success(let snapshot):
self.weatherIcon.image = UIImage(named: snapshot.assetName)
self.weatherLab.text = snapshot.text
self.localWeatherSnapshot = snapshot
self.localWeatherSnapshotKey = locationKey
self.localWeatherUpdatedAt = Date()
self.applyWeather(snapshot)
case .failure:
self.applyUnknownWeather(text: "***")
if self.localWeatherSnapshot == nil {
self.applyUnknownWeather(text: "***")
}
}
}
}
private func applyWeather(_ snapshot: MemberWeatherSnapshot) {
weatherIcon.image = UIImage(named: snapshot.assetName)
weatherLab.text = snapshot.text
}
private static func weatherLocationKey(_ coordinate: CLLocationCoordinate2D) -> String {
let latitude = Int((coordinate.latitude * 1_000).rounded())
let longitude = Int((coordinate.longitude * 1_000).rounded())
return "\(latitude):\(longitude)"
}
private func updateAppUsageVisibility(_ visible: Bool) {
if visible {
phoneReportBottomConstraint?.isActive = false
@ -345,8 +401,7 @@ class GroupMemberView2: UIView {
return
}
let snapshot = MemberWeatherService.snapshot(reportedText: weather)
weatherIcon.image = UIImage(named: snapshot.assetName)
weatherLab.text = snapshot.text
applyWeather(snapshot)
}
private func applyUnknownWeather(text: String) {

View File

@ -222,7 +222,7 @@ class HomeView2: UIView {
.centerY()
let label = UILabel()
label.text = "权限未开启,数据可能会异常"
label.text = "定位权限未开启,数据可能会异常"
label.textColor = UIColor(hexStr: "#353B4F")
label.font = .systemFont(ofSize: 12, weight: .medium)
view.addSubview(label)

View File

@ -126,6 +126,7 @@ class HomeViewController: BaseViewController {
super.viewDidAppear(animated)
UnlockCountManager.shared.refreshAuthorizationAndMonitoring()
MQTTService.shared.applicationDidBecomeActive()
rootView.groupMemberView.refreshLocalDeviceInfoIfNeeded()
let manager = AuthorizeManager.manager(type: .locationAlways)
rootView.warningView.isHidden = manager?.authorizeStatus() == .authorized
@ -353,11 +354,11 @@ class HomeViewController: BaseViewController {
self.openAppUsageReport()
}).disposed(by: disposeBag)
rootView.groupMemberView.onPhoneReportRemind = {
DLToast.show(text: "已提醒")
rootView.groupMemberView.onPhoneReportRemind = { [weak self] in
self?.remindSelectedMember(content: "请开启手机报告隐私设置")
}
rootView.groupMemberView.onAppUsageRemind = {
DLToast.show(text: "已提醒")
rootView.groupMemberView.onAppUsageRemind = { [weak self] in
self?.remindSelectedMember(content: "请开启App使用记录隐私设置")
}
//
@ -1150,6 +1151,14 @@ class HomeViewController: BaseViewController {
}
#endif
private func remindSelectedMember(content: String) {
GroupService.sendPrivilegeRemind(
content: content,
groupKey: viewModel.groupModel?.default_group_key ?? "",
userId: selectedMemberId
)
}
/// / 线 /
private func refreshSelectedMemberInfoIfNeeded(userId: String) {
guard userId == selectedMemberId,
@ -1537,6 +1546,13 @@ extension HomeViewController {
self?.removeAnnotation(userId: userId)
}
case "bubblequit":
let targetId = msg.data?.user_id ?? userId
guard !targetId.isEmpty else { return }
DispatchQueue.main.async { [weak self] in
self?.handleBubbleQuit(userId: targetId)
}
case "sos": //
guard let userId = msg.data?.user_id,
userId != AppContextManager.shared.userId,
@ -1547,6 +1563,13 @@ extension HomeViewController {
guard let member = self.viewModel.memberList.first(where: { $0.user_id == userId }) else { return }
self.showSOSReceiveAlert(for: member)
}
case "sosquit":
let targetId = msg.data?.user_id ?? userId
guard !targetId.isEmpty else { return }
DispatchQueue.main.async { [weak self] in
self?.handleSOSQuit(userId: targetId)
}
case "join", "leave", "dismiss", "kick":
break
@ -1555,6 +1578,40 @@ extension HomeViewController {
}
}
private func handleSOSQuit(userId: String) {
viewModel.updateMemberStatus(userId: userId, status: 0)
updateSelectedMemberSOSGradient()
refreshSelectedMemberInfoIfNeeded(userId: userId)
guard let alertView = sosReceiveAlertView, alertView.superview != nil else { return }
if alertView.removeMember(userId: userId) { return }
alertView.dismiss()
if sosReceiveAlertView === alertView {
sosReceiveAlertView = nil
}
stopSOSAlarm()
}
private func handleBubbleQuit(userId: String) {
viewModel.updateMemberStatus(userId: userId, status: 0)
refreshSelectedMemberInfoIfNeeded(userId: userId)
syncMemberAnnotations(viewModel.memberList)
if viewModel.isCurrentUser(id: userId) {
if let coord = trueCoordinateByUserId["current"] ?? trueCoordinateByUserId[userId],
CLLocationCoordinate2DIsValid(coord) {
updateAnnotation(
userId: "current",
coordinate: coord,
address: currentUserMember?.address ?? ""
)
}
return
}
if let coord = trueCoordinateByUserId[userId], CLLocationCoordinate2DIsValid(coord) {
let address = viewModel.memberList.first(where: { $0.user_id == userId })?.lastLocation ?? ""
updateAnnotation(userId: userId, coordinate: coord, address: address)
}
}
/// 线
private func updateOnlineCount() {
let total = viewModel.memberCount

View File

@ -71,7 +71,9 @@ class HomeViewModel {
//
func isCurrentUser(id: String) -> Bool {
id == AppContextManager.shared.userId
let memberId = id.trimmingCharacters(in: .whitespacesAndNewlines)
let selfId = AppContextManager.shared.userId.trimmingCharacters(in: .whitespacesAndNewlines)
return !memberId.isEmpty && memberId == selfId
}
//

View File

@ -612,7 +612,7 @@ final class EmojiPanelCell: UICollectionViewCell {
self.lottieView.play()
}
lockView.isHidden = AppContextManager.shared.vip > 1
lockView.isHidden = true//AppContextManager.shared.vip > 1
}
func playAnimation() {

View File

@ -75,7 +75,7 @@ class LocationPermissionPopView: UIView {
lazy var popBgView: UIView = {
let view = UIView()
view.backgroundColor = .white
view.cornerRadius = 30
view.cornerRadius = 40
return view
}()

View File

@ -93,7 +93,8 @@ final class MemberAppUsageView: UIView {
deniedOverlay.configure(
title: "TA未开启设置",
message: "无法查看app使用记录",
actionTitle: "提醒TA"
actionTitle: "提醒TA",
imageName: "PrivilegeDenied/app_usage"
)
deniedOverlay.onAction = { [weak self] in self?.onDeniedAction?() }
deniedOverlay.setVisible(denied)

View File

@ -71,7 +71,8 @@ final class MemberPhoneReportView: UIView {
deniedOverlay.configure(
title: "TA未开启设置",
message: "无法查看手机报告",
actionTitle: "提醒TA"
actionTitle: "提醒TA",
imageName: "PrivilegeDenied/phone_report"
)
deniedOverlay.onAction = { [weak self] in self?.onDeniedAction?() }
deniedOverlay.setVisible(denied)

View File

@ -53,6 +53,9 @@ final class PhoneReportDetailVC: BaseViewController {
rootView.memberCV.delegate = self
bind()
rootView.memberCV.reloadData()
DispatchQueue.main.async { [weak self] in
self?.rootView.updateMemberFadeVisibility()
}
}
private func bind() {
@ -80,6 +83,30 @@ final class PhoneReportDetailVC: BaseViewController {
})
.disposed(by: disposeBag)
viewModel.selectedMemberId
.observe(on: MainScheduler.instance)
.subscribe(onNext: { [weak self] _ in
self?.updatePrivilegeOverlay()
})
.disposed(by: disposeBag)
rootView.onRemind = { [weak self] in
guard let self else { return }
GroupService.sendPrivilegeRemind(
content: "请开启手机报告隐私设置",
groupKey: self.groupModel?.default_group_key ?? "",
userId: self.viewModel.selectedMemberId.value
)
}
updatePrivilegeOverlay()
}
private func updatePrivilegeOverlay() {
let userId = viewModel.selectedMemberId.value
let isSelf = userId == AppContextManager.shared.userId
let member = viewModel.members.first { $0.user_id == userId }
let denied = !isSelf && member?.extra.privilege.phone_report == false
rootView.setPrivilegeDenied(denied)
}
private func switchGroup() {
@ -114,4 +141,9 @@ extension PhoneReportDetailVC: UICollectionViewDataSource, UICollectionViewDeleg
let model = viewModel.members[indexPath.item]
viewModel.selectedMemberId.accept(model.user_id)
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
guard scrollView === rootView.memberCV else { return }
rootView.updateMemberFadeVisibility()
}
}

View File

@ -30,23 +30,24 @@ final class PhoneReportDetailView: UIView {
btn.extendEdgeInsets = UIEdgeInsets(top: 20, left: 20, bottom: 15, right: 18)
return btn
}()
lazy var memberCV: UICollectionView = {
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .horizontal
layout.itemSize = CGSize(width: 61, height: 90)
layout.minimumLineSpacing = 12
let cv = UICollectionView(frame: .zero, collectionViewLayout: layout)
cv.backgroundColor = .white
cv.backgroundColor = .clear
cv.showsHorizontalScrollIndicator = false
cv.contentInset = UIEdgeInsets(top: 0, left: 12, bottom: 0, right: 12)
cv.contentInset = UIEdgeInsets(top: 5, left: 5, bottom: 0, right: 5)
cv.register(GroupMemberListCell.self)
cv.applyPhoneReportCardStyle(cornerRadius: 20)
// cv.applyPhoneReportCardStyle(cornerRadius: 20)
return cv
}()
let scrollView = UIScrollView()
let contentView = UIView()
private let memberTrailingFadeView = PhoneReportMemberTrailingFadeView()
// MARK: Status
let statusTitleIcon = UIImageView(image: UIImage(named: "Home/report_phone"))
@ -65,6 +66,8 @@ final class PhoneReportDetailView: UIView {
let deviceImage = UIImageView(image: UIImage(named: "Home/phone_device"))
let brandLogoImage = UIImageView()
private var brandLogoURL = ""
private let statusDeniedOverlay = PrivilegeDeniedOverlay()
var onRemind: (() -> Void)?
// MARK: Charts
let screenSection = PhoneReportChartSectionView(
@ -74,7 +77,8 @@ final class PhoneReportDetailView: UIView {
summaryColor: UIColor(hexStr: "#00ADFE"),
barGradient: (UIColor(hexStr: "#C0EAFF"), UIColor(hexStr: "#ECF9FF")),
highlightGradient: (UIColor(hexStr: "#16B3FF"), UIColor(hexStr: "#9EDFFF")),
tipColor: UIColor(hexStr: "#20B6FF")
tipColor: UIColor(hexStr: "#20B6FF"),
tipArrowImageName: "Home/report_screen_tip_arrow"
)
let unlockSection = PhoneReportChartSectionView(
title: "APP使用次数",
@ -84,6 +88,7 @@ final class PhoneReportDetailView: UIView {
barGradient: (UIColor(hexStr: "#DDFFA1"), UIColor(hexStr: "#F2FFDC")),
highlightGradient: (UIColor(hexStr: "#AAF52A"), UIColor(hexStr: "#E8FFC1")),
tipColor: UIColor(hexStr: "#86E73D"),
tipArrowImageName: "Home/report_usage_tip_arrow",
valueSuffix: ""
)
@ -118,11 +123,28 @@ final class PhoneReportDetailView: UIView {
scrollView.addSubview(contentView)
contentView.layoutChain.edges().widthToView(scrollView)
contentView.addSubview(memberCV)
memberCV.layoutChain
let memberView = UIView()
memberView.backgroundColor = .white
memberView.applyPhoneReportCardStyle(cornerRadius: 20)
contentView.addSubview(memberView)
memberView.addSubview(memberCV)
memberView.addSubview(memberTrailingFadeView)
memberView.layoutChain
.top(8)
.edgesHorzontal(15)
.height(100)
memberCV.layoutChain
.edgesVertical()
.edgesHorzontal()
memberTrailingFadeView.layoutChain
.top()
.right()
.bottom()
.width(52)
statusTitleIcon.contentMode = .scaleAspectFit
contentView.addSubview(statusTitleIcon)
@ -147,6 +169,9 @@ final class PhoneReportDetailView: UIView {
.topToBottomOfView(statusTitleIcon, offset: 10)
.edgesHorzontal(15)
.height(175)
statusCard.addSubview(statusDeniedOverlay)
statusDeniedOverlay.layoutChain.edges()
statusDeniedOverlay.layer.cornerRadius = 20
let leftPanel = UIView()
leftPanel.backgroundColor = .clear
@ -182,7 +207,7 @@ final class PhoneReportDetailView: UIView {
.right(12)
.height(64)
deviceCard.backgroundColor = UIColor(hexStr: "#F7FBFF")
deviceCard.backgroundColor = UIColor(hexStr: "#FAFAFA")
deviceCard.cornerRadius = 12
statusCard.addSubview(deviceCard)
deviceCard.layoutChain
@ -240,6 +265,21 @@ final class PhoneReportDetailView: UIView {
.bottom(24)
}
override func layoutSubviews() {
super.layoutSubviews()
updateMemberFadeVisibility()
}
func updateMemberFadeVisibility() {
let rightInset = memberCV.adjustedContentInset.right
let maxOffsetX = max(
-memberCV.adjustedContentInset.left,
memberCV.contentSize.width - memberCV.bounds.width + rightInset
)
memberTrailingFadeView.isHidden = memberCV.contentOffset.x >= maxOffsetX - 1
|| memberCV.contentSize.width <= memberCV.bounds.width
}
func apply(_ snap: PhoneReportDetailViewModel.StatusSnapshot) {
batteryRing.configure(percent: snap.batteryPercent, text: snap.batteryText, isCharging: snap.isCharging)
estimateLab.text = "预计使用\(snap.estimateText)"
@ -256,6 +296,27 @@ final class PhoneReportDetailView: UIView {
unlockSection.setBars(snap.appUsageBars)
}
func setPrivilegeDenied(_ denied: Bool) {
let title = "TA未开启设置"
let message = "无法查看手机状态"
let action = "提醒TA"
statusDeniedOverlay.configure(
title: title,
message: message,
actionTitle: action,
imageName: "PrivilegeDenied/report_status"
)
statusDeniedOverlay.onAction = { [weak self] in self?.onRemind?() }
statusDeniedOverlay.setVisible(denied)
if denied { statusCard.bringSubviewToFront(statusDeniedOverlay) }
screenSection.setDenied(denied, imageName: "PrivilegeDenied/report_screen", onRemind: { [weak self] in
self?.onRemind?()
})
unlockSection.setDenied(denied, imageName: "PrivilegeDenied/report_usage", onRemind: { [weak self] in
self?.onRemind?()
})
}
private func updateBrandLogo(urlString: String) {
brandLogoURL = urlString
brandLogoImage.kf.cancelDownloadTask()
@ -284,10 +345,10 @@ private extension UIView {
layer.cornerRadius = cornerRadius
clipsToBounds = false
layer.masksToBounds = false
layer.shadowColor = UIColor(hexStr: "#0F2846", alpha: 0.1).cgColor
layer.shadowOffset = CGSize(width: 0, height: 2)
layer.shadowColor = UIColor(hexStr: "#E5E5E5", alpha: 0.3).cgColor
layer.shadowOffset = CGSize(width: 0, height: 0)
layer.shadowOpacity = 1
layer.shadowRadius = 10
layer.shadowRadius = 5
}
}
@ -351,7 +412,7 @@ final class PhoneStatusMiniCard: UIView {
init(iconName: String, title: String) {
super.init(frame: .zero)
backgroundColor = UIColor(hexStr: "#F7FBFF")
backgroundColor = UIColor(hexStr: "#FAFAFA")
cornerRadius = 12
icon.image = UIImage(named: iconName)
icon.contentMode = .scaleAspectFit
@ -387,7 +448,10 @@ final class PhoneReportChartSectionView: UIView {
private let barGradient: (UIColor, UIColor)
private let highlightGradient: (UIColor, UIColor)
private let tipColor: UIColor
private let tipArrowImageName: String
private let valueSuffix: String
private let card = UIView()
private let deniedOverlay = PrivilegeDeniedOverlay()
init(
title: String,
@ -397,6 +461,7 @@ final class PhoneReportChartSectionView: UIView {
barGradient: (UIColor, UIColor),
highlightGradient: (UIColor, UIColor),
tipColor: UIColor,
tipArrowImageName: String,
valueSuffix: String = ""
) {
self.summaryPrefix = summaryPrefix
@ -404,6 +469,7 @@ final class PhoneReportChartSectionView: UIView {
self.barGradient = barGradient
self.highlightGradient = highlightGradient
self.tipColor = tipColor
self.tipArrowImageName = tipArrowImageName
self.valueSuffix = valueSuffix
super.init(frame: .zero)
let titleIcon = UIImageView(image: UIImage(named: iconName))
@ -416,11 +482,13 @@ final class PhoneReportChartSectionView: UIView {
addSubview(titleLab)
titleLab.layoutChain.centerY(titleIcon).leftToRightOfView(titleIcon, offset: 6)
let card = UIView()
card.backgroundColor = .white
card.applyPhoneReportCardStyle(cornerRadius: 20)
addSubview(card)
card.layoutChain.topToBottomOfView(titleLab, offset: 10).edgesHorzontal().bottom().height(180)
card.addSubview(deniedOverlay)
deniedOverlay.layoutChain.edges()
deniedOverlay.layer.cornerRadius = 20
summaryPrefixLab.text = summaryPrefix
summaryPrefixLab.font = .systemFont(ofSize: 12, weight: .medium)
@ -436,7 +504,7 @@ final class PhoneReportChartSectionView: UIView {
summaryLab.layoutChain
.centerY(summaryPrefixLab)
.leftToRightOfView(summaryPrefixLab, offset: 4)
.right(14, relation: .lessThanOrEqual)
.right(14, relation: .greaterThanOrEqual)
card.addSubview(chart)
chart.layoutChain.topToBottomOfView(summaryPrefixLab, offset: 16).edgesHorzontal(25).bottom(14)
@ -444,6 +512,18 @@ final class PhoneReportChartSectionView: UIView {
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
func setDenied(_ denied: Bool, imageName: String, onRemind: (() -> Void)?) {
deniedOverlay.configure(
title: "TA未开启设置",
message: "无法查看手机状态",
actionTitle: "提醒TA",
imageName: imageName
)
deniedOverlay.onAction = onRemind
deniedOverlay.setVisible(denied)
if denied { card.bringSubviewToFront(deniedOverlay) }
}
func setSummary(_ text: String) {
summaryLab.text = text
}
@ -454,6 +534,7 @@ final class PhoneReportChartSectionView: UIView {
barGradient: barGradient,
highlightGradient: highlightGradient,
tipColor: tipColor,
tipArrowImageName: tipArrowImageName,
suffix: valueSuffix
)
}
@ -472,6 +553,7 @@ final class PhoneReportBarChartView: UIView {
private var barSpacing: CGFloat = 0
private var baselineY: CGFloat = 0
private let tipLab = UILabel()
private let tipArrowView = UIImageView()
private let baseline = UIView()
override init(frame: CGRect) {
@ -488,6 +570,10 @@ final class PhoneReportBarChartView: UIView {
tipLab.isHidden = true
tipLab.isUserInteractionEnabled = false
addSubview(tipLab)
tipArrowView.contentMode = .scaleAspectFit
tipArrowView.isHidden = true
tipArrowView.isUserInteractionEnabled = false
addSubview(tipArrowView)
let tap = UITapGestureRecognizer(target: self, action: #selector(handleTap(_:)))
addGestureRecognizer(tap)
@ -500,6 +586,7 @@ final class PhoneReportBarChartView: UIView {
barGradient: (UIColor, UIColor),
highlightGradient: (UIColor, UIColor),
tipColor: UIColor,
tipArrowImageName: String,
suffix: String
) {
let previous = self.bars
@ -507,6 +594,7 @@ final class PhoneReportBarChartView: UIView {
self.barGradient = barGradient
self.highlightGradient = highlightGradient
self.tipColor = tipColor
tipArrowView.image = UIImage(named: tipArrowImageName)
self.suffix = suffix
let dataChanged = previous.count != bars.count
|| zip(previous, bars).contains { $0.label != $1.label || $0.value != $1.value }
@ -540,6 +628,7 @@ final class PhoneReportBarChartView: UIView {
labelViews.removeAll()
barFrames.removeAll()
tipLab.isHidden = true
tipArrowView.isHidden = true
guard !bars.isEmpty else {
baseline.isHidden = true
return
@ -549,7 +638,11 @@ final class PhoneReportBarChartView: UIView {
let maxVal = max(bars.map(\.value).max() ?? 1, 1)
let count = bars.count
let labelH: CGFloat = 16
let chartH = bounds.height - labelH - 24
let tipVerticalPadding: CGFloat = 8
let tipArrowHeight: CGFloat = 5
let tipArrowBarSpacing: CGFloat = 6
let tipHeight = ceil(tipLab.font.lineHeight) + tipVerticalPadding
let chartH = max(4, bounds.height - labelH - tipHeight - tipArrowHeight - tipArrowBarSpacing)
let barW: CGFloat = 16
barSpacing = count > 1
? max(0, (bounds.width - barW * CGFloat(count)) / CGFloat(count - 1))
@ -577,6 +670,7 @@ final class PhoneReportBarChartView: UIView {
gradient.startPoint = CGPoint(x: 0.5, y: 0)
gradient.endPoint = CGPoint(x: 0.5, y: 1)
gradient.cornerRadius = 4
gradient.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
layer.insertSublayer(gradient, below: tipLab.layer)
barLayers.append(gradient)
@ -594,18 +688,25 @@ final class PhoneReportBarChartView: UIView {
tipLab.backgroundColor = tipColor
tipLab.sizeToFit()
let tipW = tipLab.bounds.width + 5 + 5
let tipH = tipLab.bounds.height + 4 + 4
// chart
tipLab.frame = CGRect(
x: x + barW / 2 - tipW / 2,
y: max(0, y - tipH - 4),
y: max(0, y - tipHeight - tipArrowHeight - tipArrowBarSpacing),
width: tipW,
height: tipH
height: tipHeight
)
tipLab.isHidden = false
tipArrowView.frame = CGRect(
x: x + barW / 2 - 5,
y: tipLab.frame.maxY,
width: 10,
height: tipArrowHeight
)
tipArrowView.isHidden = false
}
}
bringSubviewToFront(tipLab)
bringSubviewToFront(tipArrowView)
}
private func tipText(for bar: PhoneReportDetailViewModel.ChartBar) -> String {
@ -620,3 +721,27 @@ final class PhoneReportBarChartView: UIView {
return "\(mins)分钟"
}
}
private final class PhoneReportMemberTrailingFadeView: UIView {
override class var layerClass: AnyClass { CAGradientLayer.self }
override init(frame: CGRect) {
super.init(frame: frame)
isUserInteractionEnabled = false
let gradientLayer = layer as! CAGradientLayer
gradientLayer.colors = [
UIColor.white.withAlphaComponent(0).cgColor,
UIColor.white.withAlphaComponent(0.88).cgColor,
UIColor.white.cgColor
]
gradientLayer.locations = [0, 0.7, 1]
gradientLayer.startPoint = CGPoint(x: 0, y: 0.5)
gradientLayer.endPoint = CGPoint(x: 1, y: 0.5)
gradientLayer.cornerRadius = 20
gradientLayer.maskedCorners = [.layerMaxXMinYCorner, .layerMaxXMaxYCorner]
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

View File

@ -144,7 +144,10 @@ final class PhoneReportDetailViewModel {
batteryText: local.batteryPercent == nil ? placeholder : local.batteryText,
batteryPercent: local.batteryPercent,
isCharging: local.isCharging,
estimateText: placeholder,
estimateText: Self.estimatedUsageText(
batteryPercent: local.batteryPercent,
placeholder: placeholder
),
brightnessText: local.brightnessText,
volumeText: local.volumeText,
wifiText: local.network,
@ -162,11 +165,15 @@ final class PhoneReportDetailViewModel {
private func makeRemoteSnapshot(report: PhoneUsageReportModel) -> StatusSnapshot {
let info = report.phoneInfo
let charts = chartFields(from: report)
let batteryPercent = Self.percent(from: info?.battery)
return StatusSnapshot(
batteryText: Self.displayText(info?.battery, placeholder: placeholder),
batteryPercent: Self.percent(from: info?.battery),
batteryPercent: batteryPercent,
isCharging: false,
estimateText: placeholder,
estimateText: Self.estimatedUsageText(
batteryPercent: batteryPercent,
placeholder: placeholder
),
brightnessText: Self.displayText(info?.brightness, placeholder: placeholder),
volumeText: Self.displayText(info?.volume, placeholder: placeholder),
wifiText: Self.displayText(info?.network, placeholder: placeholder),
@ -287,6 +294,13 @@ final class PhoneReportDetailViewModel {
return max(0, min(100, Int(number.rounded())))
}
private static func estimatedUsageText(batteryPercent: Int?, placeholder: String) -> String {
guard let batteryPercent else { return placeholder }
let estimatedMinutes = batteryPercent * 9
let hours = max(1, Int((Double(estimatedMinutes) / 60).rounded()))
return "\(hours)小时"
}
private static func storageRatio(used: String?, total: String?) -> CGFloat {
guard let usedValue = firstNumber(in: used),
let totalValue = firstNumber(in: total),

View File

@ -375,7 +375,11 @@ final class ReceiveMessagePopView: UIView {
avatarView.setHeadPic(item.headPic)
messageImageView.kf.cancelDownloadTask()
if let imageURL = item.imageURL {
messageImageView.kf.setImage(with: imageURL, placeholder: item.image)
messageImageView.kf.setImage(
with: imageURL,
placeholder: item.image,
options: [.onFailureImage(.brokenNetworkImage)]
)
} else {
messageImageView.image = item.image
}

View File

@ -11,6 +11,7 @@ final class ReceiveMessageReportPopView: UIView {
private static let shared = ReceiveMessageReportPopView(
frame: CGRect(origin: .zero, size: kScreenSize)
)
private static let defaultCenterYOffset: CGFloat = 8
private let reasons = ["诈骗", "涉黄", "违规行为", "散播不良广告", "谩骂攻击", "其他"]
private var selectedReasons = Set<String>()
@ -52,17 +53,26 @@ final class ReceiveMessageReportPopView: UIView {
return view
}()
private let popView = UIView()
private let popView: UIView = {
let view = UIView()
view.backgroundColor = .clear
view.clipsToBounds = false
return view
}()
private let popBgView: UIView = {
let view = UIView()
view.backgroundColor = .white
view.layer.cornerRadius = 30
view.layer.cornerRadius = 40
view.clipsToBounds = true
return view
}()
private let logoImgView = UIImageView(image: UIImage(named: "Popup/bell"))
private let logoImgView: UIImageView = {
let view = UIImageView(image: UIImage(named: "LockDistract/horn_bird"))
view.contentMode = .scaleAspectFit
return view
}()
private let headerBgImg: UIImageView = {
let view = UIImageView(image: UIImage(named: "Popup/header_bg"))
@ -83,9 +93,9 @@ final class ReceiveMessageReportPopView: UIView {
private let optionsStack: UIStackView = {
let stack = UIStackView()
stack.axis = .vertical
stack.spacing = 22
stack.spacing = 7
stack.alignment = .fill
stack.distribution = .fillEqually
stack.distribution = .fill
return stack
}()
@ -175,24 +185,32 @@ final class ReceiveMessageReportPopView: UIView {
popView.layoutChain
.centerX()
.width(315)
.height(450)
popCenterYConstraint = popView.centerYAnchor.constraint(equalTo: centerYAnchor)
popCenterYConstraint = popView.centerYAnchor.constraint(
equalTo: centerYAnchor,
constant: Self.defaultCenterYOffset
)
popCenterYConstraint?.isActive = true
popBgView.layoutChain.edges()
popBgView.layoutChain
.top(30)
.left()
.right()
.bottom()
logoImgView.layoutChain
.left(16)
.top(-45)
.width(109)
.height(109)
.left(42)
.top()
.width(100)
.height(74)
headerBgImg.layoutChain
.edges(excludingEdge: .bottom)
.heightToWidth(100 / 315)
titleLabel.layoutChain
.top(70)
.top(48)
.edgesHorzontal(20)
optionViews = reasons.map { reason in
@ -202,30 +220,31 @@ final class ReceiveMessageReportPopView: UIView {
let rowStack = UIStackView()
rowStack.axis = .horizontal
rowStack.alignment = .fill
rowStack.distribution = .fillEqually
rowStack.spacing = 8
rowStack.distribution = .fill
rowStack.spacing = 0
rowStack.isUserInteractionEnabled = true
rowStack.addArrangedSubview(optionViews[row * 2])
rowStack.addArrangedSubview(optionViews[row * 2 + 1])
optionViews[row * 2].layoutChain.width(118)
optionsStack.addArrangedSubview(rowStack)
rowStack.layoutChain.height(36)
}
optionsStack.isUserInteractionEnabled = true
optionsStack.layoutChain
.topToBottomOfView(titleLabel, offset: 24)
.left(24)
.right(20)
.topToBottomOfView(titleLabel, offset: 18)
.left(32)
.right(18)
popBgView.bringSubviewToFront(optionsStack)
popBgView.bringSubviewToFront(inputContainer)
popBgView.bringSubviewToFront(confirmBtn)
popBgView.bringSubviewToFront(cancelBtn)
inputContainer.layoutChain
.topToBottomOfView(optionsStack, offset: 16)
.left(20)
.right(20)
.height(86)
.topToBottomOfView(optionsStack, offset: 10)
.left(30)
.right(24)
.height(80)
remarkTextView.layoutChain.edges()
placeholderLabel.layoutChain
@ -244,7 +263,7 @@ final class ReceiveMessageReportPopView: UIView {
.left(20)
.right(20)
.height(50)
.bottom(8)
.bottom()
}
private func setupActions() {
@ -266,7 +285,7 @@ final class ReceiveMessageReportPopView: UIView {
remarkTextView.text = ""
placeholderLabel.isHidden = false
optionViews.forEach { $0.isChosen = false }
popCenterYConstraint?.constant = 0
popCenterYConstraint?.constant = Self.defaultCenterYOffset
updateConfirmState()
}
@ -342,7 +361,9 @@ final class ReceiveMessageReportPopView: UIView {
let keyboardFrame = convert(frame, from: nil)
let inputFrame = inputContainer.convert(inputContainer.bounds, to: self)
let overlap = inputFrame.maxY + 16 - keyboardFrame.minY
popCenterYConstraint?.constant = overlap > 0 ? -overlap : 0
popCenterYConstraint?.constant = overlap > 0
? Self.defaultCenterYOffset - overlap
: Self.defaultCenterYOffset
UIView.animate(withDuration: duration) {
self.layoutIfNeeded()
}
@ -350,7 +371,7 @@ final class ReceiveMessageReportPopView: UIView {
@objc private func keyboardWillHide(_ notification: Notification) {
let duration = (notification.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double) ?? 0.25
popCenterYConstraint?.constant = 0
popCenterYConstraint?.constant = Self.defaultCenterYOffset
UIView.animate(withDuration: duration) {
self.layoutIfNeeded()
}
@ -432,8 +453,8 @@ final class ReceiveMessageReportOptionView: UIButton {
boxView.layoutChain
.left()
.centerY()
.width(22)
.height(22)
.width(20)
.height(20)
reasonLabel.layoutChain
.leftToRightOfView(boxView, offset: 8)
@ -455,13 +476,13 @@ final class ReceiveMessageReportOptionView: UIButton {
fatalError("init(coder:) has not been implemented")
}
private static let boxSize = CGSize(width: 22, height: 22)
private static let boxSize = CGSize(width: 20, height: 20)
private static let normalImage = UIGraphicsImageRenderer(size: boxSize).image { _ in
let rect = CGRect(origin: .zero, size: boxSize).insetBy(dx: 1, dy: 1)
UIColor.white.setFill()
UIBezierPath(ovalIn: rect).fill()
UIColor(hexStr: "#D0D5DD").setStroke()
UIColor(hexStr: "#C5C7CA").setStroke()
let border = UIBezierPath(ovalIn: rect)
border.lineWidth = 1.5
border.stroke()
@ -476,9 +497,9 @@ final class ReceiveMessageReportOptionView: UIButton {
check.lineWidth = 1.8
check.lineCapStyle = .round
check.lineJoinStyle = .round
check.move(to: CGPoint(x: 6.2, y: 11.4))
check.addLine(to: CGPoint(x: 9.4, y: 14.6))
check.addLine(to: CGPoint(x: 16.0, y: 7.8))
check.move(to: CGPoint(x: 5.5, y: 10.4))
check.addLine(to: CGPoint(x: 8.6, y: 13.5))
check.addLine(to: CGPoint(x: 14.8, y: 7.2))
UIColor.white.setStroke()
check.stroke()
}

View File

@ -179,6 +179,38 @@ final class SOSReceiveAlertView: UIView {
setNeedsLayout()
}
/// SOS `false`
@discardableResult
func removeMember(userId: String) -> Bool {
let trimmed = userId.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return true }
if trimmed == primaryMemberID {
guard let next = secondaryMembers.first else { return false }
applyPrimary(next)
removeSecondary(userId: next.user_id)
return true
}
if secondaryMembers.contains(where: { $0.user_id == trimmed }) {
removeSecondary(userId: trimmed)
}
return true
}
private func applyPrimary(_ member: GroupMemberModel) {
primaryMemberID = member.user_id
primaryAvatarView.setHeadPic(member.displayHeadPic)
nameLabel.text = member.showName.isEmpty ? "圈子成员" : member.showName
}
private func removeSecondary(userId: String) {
secondaryMembers.removeAll { $0.user_id == userId }
secondaryAvatarViews[userId]?.removeFromSuperview()
secondaryAvatarViews.removeValue(forKey: userId)
setNeedsLayout()
}
func dismiss() {
stopAnimations()
secondaryAvatarViews.values.forEach { $0.removeFromSuperview() }

View File

@ -35,6 +35,7 @@ class SearchLocationVC: BaseViewController {
DLToast.show(text: "请输入正确的手机号码")
return
}
self.rootView.phoneInputTF.resignFirstResponder()
self.requestSearchPhone(phone: phone)
self.rootView.searchProgressView.isHidden = false
self.rootView.playVideo {

View File

@ -72,7 +72,11 @@ final class LockDistractVC: BaseViewController {
if self.selectedMember?.user_id == AppContextManager.shared.userId {
AppRouter.push(Route.privacySettings)
} else {
DLToast.show(text: "已提醒")
GroupService.sendPrivilegeRemind(
content: "请开启一键锁机隐私设置",
groupKey: self.activeGroupKey,
userId: self.selectedMember?.user_id ?? ""
)
}
}

View File

@ -36,6 +36,9 @@ final class LockDistractView: UIView {
private(set) var isCurrentUser = false
private(set) var primaryAction: LockDistractPrimaryAction = .lock
private let copyMaxLength = 20
private var lockBtnHeightConstraint: NSLayoutConstraint?
private var bottomStackToAppsConstraint: NSLayoutConstraint?
private var bottomStackToStatsConstraint: NSLayoutConstraint?
override init(frame: CGRect) {
super.init(frame: frame)
@ -119,30 +122,51 @@ final class LockDistractView: UIView {
var onPrivilegeAction: (() -> Void)?
private(set) var isPrivilegeDenied = false
private let privilegeOverlay = PrivilegeDeniedOverlay()
private let appsPrivilegeOverlay = PrivilegeDeniedOverlay()
private let stylePrivilegeOverlay = PrivilegeDeniedOverlay()
func setPrivilegeDenied(_ denied: Bool, isCurrentUser: Bool) {
isPrivilegeDenied = denied
privilegeOverlay.configure(
appsPrivilegeOverlay.configure(
title: "",
message: "",
actionTitle: "",
imageName: "PrivilegeDenied/lock_apps"
)
stylePrivilegeOverlay.configure(
title: "TA未开启权限",
message: "无法查看可锁应用",
actionTitle: isCurrentUser ? "去开启" : "提醒TA"
actionTitle: isCurrentUser ? "去开启" : "提醒TA",
imageName: "PrivilegeDenied/lock_style"
)
privilegeOverlay.onAction = { [weak self] in self?.onPrivilegeAction?() }
privilegeOverlay.setVisible(denied)
stylePrivilegeOverlay.onAction = { [weak self] in self?.onPrivilegeAction?() }
appsPrivilegeOverlay.setVisible(denied)
stylePrivilegeOverlay.setVisible(denied)
if denied {
contentView.bringSubviewToFront(privilegeOverlay)
appsCard.bringSubviewToFront(appsPrivilegeOverlay)
styleCard.bringSubviewToFront(stylePrivilegeOverlay)
}
}
private func applyLockedState() {
let lockedByOthers = primaryAction == .hidden
updateLockedEmptyImage()
avatarLockView.isHidden = !isLocked
styleSectionTitle.isHidden = isLocked
styleCard.isHidden = isLocked
lockedEmptyView.isHidden = !isLocked
appsSectionTitle.isHidden = lockedByOthers
appsCard.isHidden = lockedByOthers
if lockedByOthers {
bottomStackToAppsConstraint?.isActive = false
bottomStackToStatsConstraint?.isActive = true
} else {
bottomStackToStatsConstraint?.isActive = false
bottomStackToAppsConstraint?.isActive = true
}
appCollectionView.reloadData()
lockBtn.isHidden = primaryAction == .hidden
lockBtn.isHidden = lockedByOthers
lockBtnHeightConstraint?.constant = lockedByOthers ? 0 : 56
switch primaryAction {
case .lock:
tipLab.text = "锁定功能需要在对方开启权限情况下才能使用哟~"
@ -184,7 +208,8 @@ final class LockDistractView: UIView {
bottomStack.addArrangedSubview(styleSectionTitle)
bottomStack.addArrangedSubview(styleCard)
bottomStack.addArrangedSubview(lockedEmptyView)
contentView.addSubview(privilegeOverlay)
appsCard.addSubview(appsPrivilegeOverlay)
styleCard.addSubview(stylePrivilegeOverlay)
navView.layoutChain
.edges(excludingEdge: .bottom)
@ -198,6 +223,7 @@ final class LockDistractView: UIView {
.bottom(kSafeBottomMargin)
.edgesHorzontal(30)
.height(56)
lockBtnHeightConstraint = lockBtn.jh_lastConstraint
tipRow.layoutChain
.centerX()
@ -246,18 +272,23 @@ final class LockDistractView: UIView {
.topToBottomOfView(appsCard, offset: 11)
.edgesHorzontal(16)
.bottom(20)
bottomStackToAppsConstraint = bottomStack.jh_constraint(
.top, toAttribute: .bottom, otherView: appsCard, relation: .equal
)
bottomStackToStatsConstraint = bottomStack.topAnchor.constraint(
equalTo: statsRow.bottomAnchor,
constant: 36
)
privilegeOverlay.layoutChain
.topToView(appsSectionTitle)
.left(16)
.right(16)
.bottomToView(bottomStack)
privilegeOverlay.layer.cornerRadius = 26
appsPrivilegeOverlay.layoutChain.edges()
appsPrivilegeOverlay.layer.cornerRadius = 26
stylePrivilegeOverlay.layoutChain.edges()
stylePrivilegeOverlay.layer.cornerRadius = 26
styleSectionTitle.layoutChain.left(4)
lockedEmptyView.layoutChain
.centerX()
// .height(150)
.height(150)
}
private func setupCopyLimit() {

View File

@ -57,10 +57,17 @@ final class MineView: UIView {
private lazy var headerBgView: UIView = {
let v = UIView()
v.backgroundColor = .clear
v.backgroundColor = UIColor(hexStr: "#F5FBFB")
return v
}()
lazy var navBgView: UIImageView = {
let iv = UIImageView()
iv.image = UIImage(named: "Common/navBar_bg_1")
iv.contentMode = .scaleAspectFill
return iv
}()
lazy var photoWall: MinePhotoWallView = {
MinePhotoWallView()
}()
@ -173,13 +180,14 @@ final class MineView: UIView {
}
private func setupUI() {
backgroundColor = UIColor(hexStr: "#FAFAFA")
backgroundColor = UIColor(hexStr: "#F5FBFB")
addSubview(scrollView)
scrollView.addSubview(contentView)
contentView.addSubview(headerBgView)
//
addSubview(navBgView)
addSubview(photoWall)
contentView.addSubview(vipCardView)
@ -197,6 +205,18 @@ final class MineView: UIView {
}
private func setupLayout() {
headerBgView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
headerBgView.topAnchor.constraint(equalTo: contentView.topAnchor),
headerBgView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
headerBgView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
headerBgView.bottomAnchor.constraint(equalTo: quickCardView.topAnchor)
])
navBgView.layoutChain
.edges(excludingEdge: .bottom)
.heightToWidth(160 / 375)
photoWall.layoutChain
.top(40)
.edgesHorzontal()
@ -310,14 +330,6 @@ final class MineView: UIView {
override func layoutSubviews() {
super.layoutSubviews()
//
setGradientLayer(
frame: CGRect(x: 0, y: 0, width: bounds.width, height: min(280, bounds.height * 0.35)),
startPoint: CGPoint(x: 0.5, y: 0),
endPoint: CGPoint(x: 0.5, y: 1),
colors: [UIColor(hexStr: "#D8F4FF"), UIColor(hexStr: "#FAFAFA")],
locations: [0, 1]
)
refreshTableViewHeight()
}

View File

@ -99,10 +99,9 @@ final class MoodStatusVC: BaseViewController {
.subscribe(onNext: { [weak self] _ in
self?.viewModel.endSaving()
AppContextManager.shared.updateAccountMood(moodIndex)
DLToast.show(text: "保存成功") {
AppRouter.shared.popOrDismiss()
NotificationCenter.default.post(name: .RefreshGroupInfoNotification, object: nil)
}
self?.rootView.setSaving(false)
DLToast.show(text: "保存成功")
NotificationCenter.default.post(name: .RefreshGroupInfoNotification, object: nil)
}, onError: { [weak self] _ in
self?.viewModel.endSaving()
self?.rootView.setSaving(false)

View File

@ -30,7 +30,6 @@ class AvatarIconListVC: BaseViewController {
let selected = self.rootView.selectedHeadPic
guard !selected.isEmpty else { return }
self.onSelectIcon?(selected)
AppRouter.shared.popOrDismiss()
}).disposed(by: disposeBag)
rootView.onBadgeTap = { [weak self] in
@ -84,7 +83,6 @@ class AvatarIconListVC: BaseViewController {
guard let self else { return }
self.rootView.setPreviewImage(image)
self.onSelectIcon?(url)
AppRouter.shared.popOrDismiss()
}, onError: { error in
DLToast.show(text: error.gatewayMessage ?? "更换失败")
})

View File

@ -215,7 +215,11 @@ final class PigeonHistoryCell: UITableViewCell {
if let image = item.image, !isVoice {
photoView.image = image
} else if let mediaURL = item.mediaURL, !isVoice {
photoView.kf.setImage(with: mediaURL, placeholder: nil)
photoView.kf.setImage(
with: mediaURL,
placeholder: nil,
options: [.onFailureImage(.brokenNetworkImage)]
)
} else {
photoView.image = nil
}

View File

@ -679,7 +679,7 @@ private extension PigeonMessageView {
if selectedMemberIds.contains(id) {
selectedMemberIds.remove(id)
} else if !member.extra.privilege.pigeon_mail {
DLToast.show(text: "该用户未开启设置,无法接收飞鸽传书")
DLToast.show(text: "TA没有开启隐私设置\n无法使用飞鸽传书")
return
} else {
selectedMemberIds.insert(id)

View File

@ -235,4 +235,40 @@ struct GroupService {
.do(onSuccess: notifyGroupInfoChange(operation: "dismiss", requestData: requestData))
.asObservable()
}
///
static func remindPrivilege(
content: String,
groupKey: String,
userId: String,
title: String = "请开启权限"
) -> Observable<ResponseModel> {
let api = GroupAPI.privilegeRemind(
title: title,
content: content,
groupKey: groupKey,
userId: userId
).multiTarget
return APIProvider.request(token: api)
.map(ResponseModel.self)
.asObservable()
}
static func sendPrivilegeRemind(content: String, groupKey: String, userId: String) {
let key = groupKey.trimmingCharacters(in: .whitespacesAndNewlines)
let uid = userId.trimmingCharacters(in: .whitespacesAndNewlines)
guard !key.isEmpty, !uid.isEmpty else { return }
DLToast.showLoading()
remindPrivilege(content: content, groupKey: key, userId: uid)
.subscribe(onNext: { response in
guard response.code == "0" else {
DLToast.showError(text: response.message ?? "提醒失败")
return
}
DLToast.show(text: "已提醒")
}, onError: { error in
DLToast.show(text: error.localizedDescription)
})
.disposed(by: disposeBag)
}
}

View File

@ -331,8 +331,6 @@ public class DLToast {
if let toastView = superView.findSubView(with: DLToastConfig.tag) as? DLToastView {
toastView.removeFromSuperview()
}
let config = config
config.containerRadius = icon == nil ? 4 : config.containerRadius
let toastView = DLToastView(iconImage: icon, text: text, config: config, style: style)
toastView.tag = DLToastConfig.tag
toastView.completion = completion