Skip to content

Commit

Permalink
Add expiration label to wireless users profile screen (#1848)
Browse files Browse the repository at this point in the history
* Add WirelessExpirationTimeFormatter

* Add expiration label to wireless users profile screen
  • Loading branch information
daehn committed Mar 12, 2018
1 parent f674f8d commit 0dcfdbd
Show file tree
Hide file tree
Showing 6 changed files with 220 additions and 3 deletions.
114 changes: 114 additions & 0 deletions Wire-iOS Tests/WirelessExpirationTimeFormatterTests.swift
@@ -0,0 +1,114 @@
//
// Wire
// Copyright (C) 2018 Wire Swiss GmbH
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see http://www.gnu.org/licenses/.
//

import XCTest
@testable import Wire

class WirelessExpirationTimeFormatterTests: XCTestCase {

func testExpirationTimeFormatting_LargerThan2Hours() {
assert(remainingTime: 12_000, expected: "4h left")
}

func testExpirationTimeFormatting_2Hours() {
assert(remainingTime: 7_200, expected: "3h left")
}

func testExpirationTimeFormatting_91Minutes() {
assert(remainingTime: 5_460, expected: "2h left")
}

func testExpirationTimeFormatting_90Minutes() {
assert(remainingTime: 5_400, expected: "1.5h left")
}

func testExpirationTimeFormatting_89Minutes() {
assert(remainingTime: 5_340, expected: "1.5h left")
}

func testExpirationTimeFormatting_61Minutes() {
assert(remainingTime: 3_660, expected: "1.5h left")
}

func testExpirationTimeFormatting_60Minutes() {
assert(remainingTime: 3_600, expected: "1h left")
}

func testExpirationTimeFormatting_59Minutes() {
assert(remainingTime: 3_540, expected: "1h left")
}

func testExpirationTimeFormatting_46Minutes() {
assert(remainingTime: 2_760, expected: "1h left")
}

func testExpirationTimeFormatting_45Minutes() {
assert(remainingTime: 2_700, expected: "1h left")
}

func testExpirationTimeFormatting_44Minutes() {
assert(remainingTime: 2_640, expected: "Less than 45m left")
}

func testExpirationTimeFormatting_31Minutes() {
assert(remainingTime: 1_860, expected: "Less than 45m left")
}

func testExpirationTimeFormatting_30Minutes() {
assert(remainingTime: 1_800, expected: "Less than 45m left")
}

func testExpirationTimeFormatting_29Minutes() {
assert(remainingTime: 1_740, expected: "Less than 30m left")
}

func testExpirationTimeFormatting_16Minutes() {
assert(remainingTime: 960, expected: "Less than 30m left")
}

func testExpirationTimeFormatting_15Minutes() {
assert(remainingTime: 900, expected: "Less than 30m left")
}

func testExpirationTimeFormatting_14Minutes() {
assert(remainingTime: 840, expected: "Less than 15m left")
}

func testExpirationTimeFormatting_5Minutes() {
assert(remainingTime: 300, expected: "Less than 15m left")
}

func testExpirationTimeFormatting_1Minute() {
assert(remainingTime: 60, expected: "Less than 15m left")
}

func testExpirationTimeFormatting_0Minutes() {
assert(remainingTime: 0, expected: nil)
}

func testExpirationTimeFormatting_NegativeValue() {
assert(remainingTime: -10, expected: nil)
}

// MARK: - Helper

private func assert(remainingTime: TimeInterval, expected: String?, file: StaticString = #file, line: UInt = #line) {
let result = WirelessExpirationTimeFormatter.shared.string(for: remainingTime)
XCTAssertEqual(result, expected, file: file, line: line)
}
}
8 changes: 8 additions & 0 deletions Wire-iOS.xcodeproj/project.pbxproj
Expand Up @@ -919,6 +919,8 @@
CEC0FE3B1C4FBF120093BF0D /* ConversationIgnoredDeviceCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEC0FE3A1C4FBF120093BF0D /* ConversationIgnoredDeviceCell.swift */; };
CEE02EAD1DDDEF3A00BA2BE2 /* avs.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CEE02EAB1DDDD15200BA2BE2 /* avs.framework */; };
CEEEAF071CB562BF00111759 /* PlaceholderConversationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEEEAF061CB562BF00111759 /* PlaceholderConversationViewController.swift */; };
D50892FB2056BD51004D3AE2 /* ZMUser+ExpirationTimeFormatting.swift in Sources */ = {isa = PBXBuildFile; fileRef = D50892FA2056BD51004D3AE2 /* ZMUser+ExpirationTimeFormatting.swift */; };
D50892FD2056C2AA004D3AE2 /* WirelessExpirationTimeFormatterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D50892FC2056C2AA004D3AE2 /* WirelessExpirationTimeFormatterTests.swift */; };
D5168F282008ED0700F8222A /* KeyboardBlockObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5168F272008ED0700F8222A /* KeyboardBlockObserver.swift */; };
D5168F2A2008F3EF00F8222A /* UIEdgeInsets+Adjusting.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5168F292008F3EF00F8222A /* UIEdgeInsets+Adjusting.swift */; };
D5168F2C200CBBF300F8222A /* Analytics+TeamInvites.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5168F2B200CBBF300F8222A /* Analytics+TeamInvites.swift */; };
Expand Down Expand Up @@ -2508,6 +2510,8 @@
CEC0FE3A1C4FBF120093BF0D /* ConversationIgnoredDeviceCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConversationIgnoredDeviceCell.swift; sourceTree = "<group>"; };
CEE02EAB1DDDD15200BA2BE2 /* avs.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = avs.framework; path = Carthage/Build/iOS/avs.framework; sourceTree = "<group>"; };
CEEEAF061CB562BF00111759 /* PlaceholderConversationViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlaceholderConversationViewController.swift; sourceTree = "<group>"; };
D50892FA2056BD51004D3AE2 /* ZMUser+ExpirationTimeFormatting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ZMUser+ExpirationTimeFormatting.swift"; sourceTree = "<group>"; };
D50892FC2056C2AA004D3AE2 /* WirelessExpirationTimeFormatterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WirelessExpirationTimeFormatterTests.swift; sourceTree = "<group>"; };
D5168F272008ED0700F8222A /* KeyboardBlockObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardBlockObserver.swift; sourceTree = "<group>"; };
D5168F292008F3EF00F8222A /* UIEdgeInsets+Adjusting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIEdgeInsets+Adjusting.swift"; sourceTree = "<group>"; };
D5168F2B200CBBF300F8222A /* Analytics+TeamInvites.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Analytics+TeamInvites.swift"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -4614,6 +4618,7 @@
8762BE141D8978F70040E4A5 /* ZMAccentColor+Additions.swift */,
87F18BD11E03F13900C69D9B /* ZMLocationMessageData+Coordinates.swift */,
8787D1831EA74F5B00254D02 /* ZMUser+Additions.swift */,
D50892FA2056BD51004D3AE2 /* ZMUser+ExpirationTimeFormatting.swift */,
);
path = syncengine;
sourceTree = "<group>";
Expand Down Expand Up @@ -5030,6 +5035,7 @@
BFAD3A1B1CD1078500F0FBED /* Resources */,
BF29C9851C6E357100601EE7 /* ZMSnapshotTestCase.h */,
BF5127141CC9118F00F23DEA /* ZMSnapshotTestCase+Swift.swift */,
D50892FC2056C2AA004D3AE2 /* WirelessExpirationTimeFormatterTests.swift */,
BF29C9861C6E357100601EE7 /* ZMSnapshotTestCase.m */,
BFFE943D1E7839EB0025AD75 /* CoreDataSnapshotTestCase.swift */,
F194CA181E717142004C5E56 /* VoiceChannelOverlayTests.swift */,
Expand Down Expand Up @@ -6857,6 +6863,7 @@
7C6878DB201B3785003A0C7A /* StartUIViewController+SearchResults.swift in Sources */,
87A15FDF1E08318B00AED79B /* CollectionLoadingCell.swift in Sources */,
F19C3A8F20513532004387E4 /* DatabaseStatisticsController.swift in Sources */,
D50892FB2056BD51004D3AE2 /* ZMUser+ExpirationTimeFormatting.swift in Sources */,
870815881BF4EAD100D321BC /* SettingsTableViewController.swift in Sources */,
EF2127261FB9DFE300625A9B /* RegistrationRootViewController.m in Sources */,
BF7ED2DE1DF59974003A4397 /* Analytics+Usernames.swift in Sources */,
Expand Down Expand Up @@ -7111,6 +7118,7 @@
BFA13E1A1D4B66EE00F0A91B /* ImageMessageCellTests.swift in Sources */,
BFA13E161D4A5F7100F0A91B /* ConfirmAssetViewControllerTests.swift in Sources */,
EF7CCAFD204E96390050325B /* MockUser+PointOfView.swift in Sources */,
D50892FD2056C2AA004D3AE2 /* WirelessExpirationTimeFormatterTests.swift in Sources */,
160287AD1E4351FC0036FC5B /* MockVoiceChannel.swift in Sources */,
87BEB0E01C734DA60094BFE9 /* MockLoader.m in Sources */,
F127BD5A1E2667170093B2F1 /* MockCollection.swift in Sources */,
Expand Down
3 changes: 3 additions & 0 deletions Wire-iOS/Resources/Base.lproj/Localizable.strings
Expand Up @@ -926,6 +926,9 @@

"guest_room.share.message" = "Join me in a conversation on Wire:\n%@";

"guest_room.expiration.hours_left" = "%@h left";
"guest_room.expiration.less_than_minutes_left" = "Less than %@m left";

// Registration

"registration.title" = "Registration";
Expand Down
5 changes: 5 additions & 0 deletions Wire-iOS/Resources/Classy/stylesheet.cas
Expand Up @@ -1304,6 +1304,11 @@ ProfileDetailsViewController {
textColor: $color-text-foreground;
font: $font-normal-light;
}

remainingTimeLabel: @{
textColor: $color-text-dimmed;
font: $font-normal-semibold;
}
}

/* Profile Devices */
Expand Down
@@ -0,0 +1,75 @@
////
// Wire
// Copyright (C) 2018 Wire Swiss GmbH
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see http://www.gnu.org/licenses/.
//

import Foundation

fileprivate extension TimeInterval {
var hours: Double {
return self / 3600
}

var minutes: Double {
return self / 60
}
}

final class WirelessExpirationTimeFormatter {
static let shared = WirelessExpirationTimeFormatter()
private let numberFormatter: NumberFormatter = {
let formatter = NumberFormatter()
formatter.maximumFractionDigits = 1
return formatter
}()

func string(for user: ZMUser) -> String? {
return string(for: user.expiresAfter)
}

func string(for interval: TimeInterval) -> String? {
guard interval > 0 else { return nil }
let (hoursLeft, minutesLeft) = (interval.hours, interval.minutes)
guard hoursLeft < 2 else { return localizedHours(floor(hoursLeft) + 1) }

if hoursLeft > 1 {
let extraMinutes = minutesLeft - 60
return localizedHours(extraMinutes > 30 ? 2 : 1.5)
}

switch minutesLeft {
case 45...Double.greatestFiniteMagnitude: return localizedHours(1)
case 30..<45: return localizedMinutes(45)
case 15..<30: return localizedMinutes(30)
default: return localizedMinutes(15)
}
}

private func localizedMinutes(_ minutes: Double) -> String {
return "guest_room.expiration.less_than_minutes_left".localized(args: String(format: "%.0f", minutes))
}

private func localizedHours(_ hours: Double) -> String {
let localizedHoursString = numberFormatter.string(from: NSNumber(value: hours)) ?? "\(hours)"
return "guest_room.expiration.hours_left".localized(args: localizedHoursString)
}
}

extension ZMUser {
@objc var expirationDisplayString: String? {
return WirelessExpirationTimeFormatter.shared.string(for: self)
}
}
Expand Up @@ -84,6 +84,7 @@ @interface ProfileDetailsViewController ()
@property (nonatomic) UIView *footerView;
@property (nonatomic) UIView *stackViewContainer;
@property (nonatomic) GuestLabelIndicator *teamsGuestIndicator;
@property (nonatomic) UILabel *remainingTimeLabel;
@property (nonatomic) BOOL showGuestLabel;
@property (nonatomic) AvailabilityTitleView *availabilityView;
@property (nonatomic) UICustomSpacingStackView *stackView;
Expand Down Expand Up @@ -123,15 +124,26 @@ - (void)setupViews
[self.view addSubview:self.stackViewContainer];
self.teamsGuestIndicator.hidden = !self.showGuestLabel;
self.availabilityView.hidden = !ZMUser.selfUser.isTeamMember || self.fullUser.availability == AvailabilityNone;
self.remainingTimeLabel = [[UILabel alloc] initForAutoLayout];
NSString *remainingTimeString = self.fullUser.expirationDisplayString;
self.remainingTimeLabel.text = remainingTimeString;
self.remainingTimeLabel.hidden = nil == remainingTimeString;

self.stackView = [[UICustomSpacingStackView alloc] initWithCustomSpacedArrangedSubviews:@[self.userImageView, self.teamsGuestIndicator, self.availabilityView]];
self.stackView = [[UICustomSpacingStackView alloc] initWithCustomSpacedArrangedSubviews:@[self.userImageView, self.teamsGuestIndicator, self.remainingTimeLabel, self.availabilityView]];
self.stackView.axis = UILayoutConstraintAxisVertical;
self.stackView.spacing = 0;
self.stackView.alignment = UIStackViewAlignmentCenter;
[self.stackViewContainer addSubview:self.stackView];

[self.stackView wr_addCustomSpacing:(self.teamsGuestIndicator.isHidden ? 32 : 32) after:self.userImageView];
[self.stackView wr_addCustomSpacing:(self.availabilityView.isHidden ? 40 : 32) after:self.teamsGuestIndicator];
[self.stackView wr_addCustomSpacing:32 after:self.userImageView];

if (self.remainingTimeLabel.isHidden) {
[self.stackView wr_addCustomSpacing:(self.availabilityView.isHidden ? 40 : 32) after:self.teamsGuestIndicator];
} else {
[self.stackView wr_addCustomSpacing:8 after:self.teamsGuestIndicator];
[self.stackView wr_addCustomSpacing:(self.availabilityView.isHidden ? 40 : 32) after:self.remainingTimeLabel];
}

[self.stackView wr_addCustomSpacing:32 after:self.availabilityView];
}

Expand Down

0 comments on commit 0dcfdbd

Please sign in to comment.