Skip to content

Commit

Permalink
CC-7053: UI changes for hidden nodes in nodecollectionviewcell
Browse files Browse the repository at this point in the history
  • Loading branch information
dg-mega committed Apr 30, 2024
1 parent 53b0b8e commit b6545c3
Show file tree
Hide file tree
Showing 12 changed files with 341 additions and 71 deletions.
208 changes: 181 additions & 27 deletions MEGAUnitTests/CloudDrive/Node/NodeCollectionViewCellViewModelTests.swift
Original file line number Diff line number Diff line change
@@ -1,70 +1,224 @@
import Combine
@testable import MEGA
import MEGADomain
import MEGADomainMock
import MEGAPresentation
import MEGAPresentationMock
import MEGASdk
import MEGASDKRepoMock
import XCTest

final class NodeCollectionViewCellViewModelTests: XCTestCase {

func testIsNodeVideo_videoName_shouldBeTrue() {
let mockUsecase = MockMediaUseCase(isStringVideo: true)
let viewModel = NodeCollectionViewCellViewModel(mediaUseCase: mockUsecase)
let viewModel = sut()

XCTAssertTrue(viewModel.isNodeVideo(name: "video.mp4"))
}

func testIsNodeVideo_imageName_shouldBeFalse() {
let mockUsecase = MockMediaUseCase(isStringVideo: false)
let viewModel = NodeCollectionViewCellViewModel(mediaUseCase: mockUsecase)
let viewModel = sut()

XCTAssertFalse(viewModel.isNodeVideo(name: "image.png"))
}

func testIsNodeVideo_noName_shouldBeFalse() {
let mockUsecase = MockMediaUseCase(isStringVideo: false)
let viewModel = NodeCollectionViewCellViewModel(mediaUseCase: mockUsecase)
let viewModel = sut()

XCTAssertFalse(viewModel.isNodeVideo(name: ""))
}

func testIsNodeVideoWithValidDuration_withVideo_validDuration_shouldBeTrue() {
let mockUsecase = MockMediaUseCase(isStringVideo: true)
let viewModel = NodeCollectionViewCellViewModel(mediaUseCase: mockUsecase)
let mockNode = NodeEntity(name: "video.mp4", handle: 1, duration: 10)
let viewModel = sut(node: mockNode)

let mockNode = MockNode(handle: 1, name: "video.mp4", duration: 10)
XCTAssertTrue(viewModel.isNodeVideoWithValidDuration(node: mockNode))
XCTAssertTrue(viewModel.isNodeVideoWithValidDuration())
}

func testIsNodeVideoWithValidDuration_withVideo_zeroDuration_shouldBeTrue() {
let mockUsecase = MockMediaUseCase(isStringVideo: true)
let viewModel = NodeCollectionViewCellViewModel(mediaUseCase: mockUsecase)
let mockNode = NodeEntity(name: "video.mp4", handle: 1, duration: 0)
let viewModel = sut(node: mockNode)

let mockNode = MockNode(handle: 1, name: "video.mp4", duration: 0)
XCTAssertTrue(viewModel.isNodeVideoWithValidDuration(node: mockNode))
XCTAssertTrue(viewModel.isNodeVideoWithValidDuration())
}

func testIsNodeVideoWithValidDuration_withVideo_invalidDuration_shouldBeFalse() {
let mockUsecase = MockMediaUseCase(isStringVideo: true)
let viewModel = NodeCollectionViewCellViewModel(mediaUseCase: mockUsecase)
let mockNode = NodeEntity(name: "video.mp4", handle: 1, duration: -1)
let viewModel = sut(node: mockNode)

let mockNode = MockNode(handle: 1, name: "video.mp4", duration: -1)
XCTAssertFalse(viewModel.isNodeVideoWithValidDuration(node: mockNode))
XCTAssertFalse(viewModel.isNodeVideoWithValidDuration())
}

func testIsNodeVideoWithValidDuration_notVideo_shouldBeFalse() {
let mockUsecase = MockMediaUseCase(isStringVideo: false)
let viewModel = NodeCollectionViewCellViewModel(mediaUseCase: mockUsecase)
let mockNode = NodeEntity(name: "image.png", handle: 1, duration: 0)
let viewModel = sut(node: mockNode)

let mockNode = MockNode(handle: 1, name: "image.png", duration: 0)
XCTAssertFalse(viewModel.isNodeVideoWithValidDuration(node: mockNode))
XCTAssertFalse(viewModel.isNodeVideoWithValidDuration())
}

func testIsNodeVideoWithValidDuration_noName_shouldBeFalse() {
let mockUsecase = MockMediaUseCase(isStringVideo: false)
let viewModel = NodeCollectionViewCellViewModel(mediaUseCase: mockUsecase)
let mockNode = NodeEntity(name: "", handle: 1, duration: 0)
let viewModel = sut(node: mockNode)

XCTAssertFalse(viewModel.isNodeVideoWithValidDuration())
}

func testHasThumbnail_NodeEntityHasThumbnail_shouldBeTrue() {
let mockNode = NodeEntity(handle: 1, hasThumbnail: true)
let viewModel = sut(node: mockNode)

XCTAssertTrue(viewModel.hasThumbnail)
}

func testHasThumbnail_nodeIsNil_shouldBeFalse() {
let viewModel = sut(node: nil)

XCTAssertFalse(viewModel.hasThumbnail)
}

func testConfigureCell_whenFeatureFlagOnAndNodeIsNil_shouldSetIsSensitiveFalse() async {
let viewModel = sut(
node: NodeEntity(handle: 1, isMarkedSensitive: true),
isFromSharedItem: true,
nodeUseCase: MockNodeDataUseCase(isInheritingSensitivityResult: .success(false)),
featureFlagHiddenNodes: true)

await viewModel.configureCell().value

let expectation = expectation(description: "viewModel.isSensitive should return value")
let subscription = viewModel.$isSensitive
.debounce(for: .milliseconds(500), scheduler: DispatchQueue.main)
.sink { isSensitive in
XCTAssertFalse(isSensitive)
expectation.fulfill()
}

await fulfillment(of: [expectation], timeout: 1)

subscription.cancel()
}

func testConfigureCell_whenFeatureFlagOnAndIsFromSharedItem_shouldSetIsSensitiveFalse() async {
let viewModel = sut(
node: nil,
nodeUseCase: MockNodeDataUseCase(isInheritingSensitivityResult: .success(false)),
featureFlagHiddenNodes: true)

await viewModel.configureCell().value

let expectation = expectation(description: "viewModel.isSensitive should return value")
let subscription = viewModel.$isSensitive
.debounce(for: .milliseconds(500), scheduler: DispatchQueue.main)
.sink { isSensitive in
XCTAssertFalse(isSensitive)
expectation.fulfill()
}

await fulfillment(of: [expectation], timeout: 1)

subscription.cancel()
}

func testConfigureCell_whenFeatureFlagOnAndNodeIsSensitive_shouldSetIsSensitiveTrue() async {
let node = NodeEntity(handle: 1, isMarkedSensitive: true)
let viewModel = sut(
node: node,
nodeUseCase: MockNodeDataUseCase(isInheritingSensitivityResult: .success(false)),
featureFlagHiddenNodes: true)

let mockNode = MockNode(handle: 1, name: "", duration: 0)
XCTAssertFalse(viewModel.isNodeVideoWithValidDuration(node: mockNode))
await viewModel.configureCell().value

let expectation = expectation(description: "viewModel.isSensitive should return value")
let subscription = viewModel.$isSensitive
.first { $0 }
.sink { isSensitive in
XCTAssertTrue(isSensitive)
expectation.fulfill()
}

await fulfillment(of: [expectation], timeout: 1)

subscription.cancel()
}

func testConfigureCell_whenFeatureFlagOffAndNodeIsSensitive_shouldSetIsSensitiveFalse() async {
let node = NodeEntity(handle: 1, isMarkedSensitive: true)
let viewModel = sut(
node: node,
nodeUseCase: MockNodeDataUseCase(isInheritingSensitivityResult: .success(false)),
featureFlagHiddenNodes: false)

await viewModel.configureCell().value

let expectation = expectation(description: "viewModel.isSensitive should return value")
let subscription = viewModel.$isSensitive
.debounce(for: 0.5, scheduler: DispatchQueue.main)
.first { !$0 }
.sink { isSensitive in
XCTAssertFalse(isSensitive)
expectation.fulfill()
}

await fulfillment(of: [expectation], timeout: 1)

subscription.cancel()
}

func testConfigureCell_whenFeatureFlagOnAndNodeInheritedSensitivity_shouldSetIsSensitiveTrue() async {
let node = NodeEntity(handle: 1, isMarkedSensitive: false)
let viewModel = sut(
node: node,
nodeUseCase: MockNodeDataUseCase(isInheritingSensitivityResult: .success(true)),
featureFlagHiddenNodes: true)

await viewModel.configureCell().value

let expectation = expectation(description: "viewModel.isSensitive should return value")
let subscription = viewModel.$isSensitive
.first { $0 }
.sink { isSensitive in
XCTAssertTrue(isSensitive)
expectation.fulfill()
}

await fulfillment(of: [expectation], timeout: 1)

subscription.cancel()
}

func testConfigureCell_whenFeatureFlagOffAndNodeInheritedSensitivity_shouldSetIsSensitiveFalse() async {
let node = NodeEntity(handle: 1, isMarkedSensitive: false)
let viewModel = sut(
node: node,
nodeUseCase: MockNodeDataUseCase(isInheritingSensitivityResult: .success(true)),
featureFlagHiddenNodes: false)

await viewModel.configureCell().value

let expectation = expectation(description: "viewModel.isSensitive should return value")
let subscription = viewModel.$isSensitive
.debounce(for: 0.5, scheduler: DispatchQueue.main)
.first { !$0 }
.sink { isSensitive in
XCTAssertFalse(isSensitive)
expectation.fulfill()
}

await fulfillment(of: [expectation], timeout: 1)

subscription.cancel()
}
}

extension NodeCollectionViewCellViewModelTests {
private func sut(node: NodeEntity? = nil,
isFromSharedItem: Bool = false,
nodeUseCase: some NodeUseCaseProtocol = MockNodeDataUseCase(),
featureFlagHiddenNodes: Bool = false) -> NodeCollectionViewCellViewModel {
NodeCollectionViewCellViewModel(
node: node,
isFromSharedItem: isFromSharedItem,
nodeUseCase: nodeUseCase,
featureFlagProvider: MockFeatureFlagProvider(list: [.hiddenNodes: featureFlagHiddenNodes]))
}
}
5 changes: 3 additions & 2 deletions iMEGA/Cloud drive/Cells/FileNodeCollectionViewCell.xib
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="22505" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="32700.99.1234" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22504"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22684"/>
<capability name="collection view cell content view" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
Expand Down Expand Up @@ -264,6 +264,7 @@
<outlet property="infoLabel" destination="QM6-Pq-Dgr" id="Khl-AX-dld"/>
<outlet property="labelImageView" destination="2iL-Ym-N6m" id="Pac-BH-Wcm"/>
<outlet property="labelView" destination="DKb-cU-AuO" id="uvy-fe-bXU"/>
<outlet property="labelsContainerStackView" destination="fkb-nZ-O3q" id="eXk-pZ-DNs"/>
<outlet property="linkImageView" destination="WOI-Er-cT4" id="4wN-oe-aXD"/>
<outlet property="linkView" destination="loU-O2-R2l" id="VAo-hx-4Ze"/>
<outlet property="moreButton" destination="imJ-KD-GDx" id="Jep-ij-73g"/>
Expand Down
64 changes: 54 additions & 10 deletions iMEGA/Cloud drive/Cells/NodeCollectionViewCell+Additions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,59 @@ import MEGADomain
import MEGASDKRepo
import UIKit

@objc extension NodeCollectionViewCell {
extension NodeCollectionViewCell {

func createNodeCollectionCellViewModel() -> NodeCollectionViewCellViewModel {
let mediaUseCase = MediaUseCase(fileSearchRepo: FilesSearchRepository.newRepo,
videoMediaUseCase: VideoMediaUseCase(videoMediaRepository: VideoMediaRepository.newRepo))
return NodeCollectionViewCellViewModel(mediaUseCase: mediaUseCase)
open override func prepareForReuse() {
super.prepareForReuse()

cancellables = []

[thumbnailImageView, thumbnailIconView, topNodeIconsView, labelsContainerStackView]
.forEach { $0?.alpha = 1 }
}

@objc func createViewModel(node: MEGANode?, isFromSharedItem: Bool) -> NodeCollectionViewCellViewModel {
NodeCollectionViewCellViewModel(
node: node?.toNodeEntity(),
isFromSharedItem: isFromSharedItem,
nodeUseCase: NodeUseCase(
nodeDataRepository: NodeDataRepository.newRepo,
nodeValidationRepository: NodeValidationRepository.newRepo,
nodeRepository: NodeRepository.newRepo))
}

@objc func bind(viewModel: NodeCollectionViewCellViewModel) {
self.viewModel = viewModel

viewModel.configureCell()

cancellables = [
viewModel
.$isSensitive
.removeDuplicates()
.receive(on: DispatchQueue.main)
.sink { [weak self] in self?.configureBlur(isSensitive: $0) }
]

}

private func configureBlur(isSensitive: Bool) {
let alpha: CGFloat = isSensitive ? 0.5 : 1
[
viewModel.hasThumbnail ? nil : thumbnailImageView,
thumbnailIconView,
topNodeIconsView,
labelsContainerStackView
].forEach { $0?.alpha = alpha }

if viewModel.hasThumbnail, isSensitive {
thumbnailImageView?.addBlurToView(style: .systemUltraThinMaterial)
} else {
thumbnailImageView?.removeBlurFromView()
}
}

func setDurationForVideo(path: String) {
@objc func setDurationForVideo(path: String) {
let asset = AVURLAsset(url: URL(fileURLWithPath: path, isDirectory: false))
asset.loadValuesAsynchronously(forKeys: ["duration"]) {
DispatchQueue.main.async {
Expand All @@ -35,7 +79,7 @@ import UIKit
}
}

func setThumbnail(url: URL) {
@objc func setThumbnail(url: URL) {
let fileAttributeGenerator = FileAttributeGenerator(sourceURL: url)
Task { @MainActor in
guard let image = await fileAttributeGenerator.requestThumbnail() else { return }
Expand All @@ -44,7 +88,7 @@ import UIKit
}
}

func setupTokenColors() {
@objc func setupTokenColors() {
nameLabel?.textColor = TokenColors.Text.primary
infoLabel?.textColor = TokenColors.Text.secondary
durationLabel?.textColor = TokenColors.Button.primary
Expand All @@ -70,7 +114,7 @@ import UIKit
videoIconView?.tintColor = TokenColors.Icon.secondary
}

func setupThumbnailBackground() {
@objc func setupThumbnailBackground() {
if UIColor.isDesignTokenEnabled() {
topNodeIconsView?.backgroundColor = TokenColors.Background.surface2
thumbnailImageView?.backgroundColor = TokenColors.Background.surface1
Expand All @@ -93,7 +137,7 @@ import UIKit
}
}

func updateSelection() {
@objc func updateSelection() {
if moreButton?.isHidden ?? false && self.isSelected {
selectImageView?.image = UIImage(resource: .thumbnailSelected)
self.contentView.layer.borderColor = UIColor.mnz_green00A886().cgColor
Expand Down
4 changes: 3 additions & 1 deletion iMEGA/Cloud drive/Cells/NodeCollectionViewCell.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,10 @@ NS_ASSUME_NONNULL_BEGIN
@property (weak, nonatomic) IBOutlet UIView *topNodeIconsView;
@property (weak, nonatomic) IBOutlet UIButton *moreButton;
@property (weak, nonatomic) IBOutlet UIImageView *selectImageView;
@property (weak, nonatomic) IBOutlet UIStackView *labelsContainerStackView;
@property (strong, nonatomic) NSSet<id> *cancellables;

- (void)configureCellForNode:(MEGANode *)node allowedMultipleSelection:(BOOL)multipleSelection sdk:(MEGASdk *)sdk delegate:(id<NodeCollectionViewCellDelegate> _Nullable)delegate;
- (void)configureCellForNode:(MEGANode *)node allowedMultipleSelection:(BOOL)multipleSelection isFromSharedItem:(BOOL)isFromSharedItem sdk:(MEGASdk *)sdk delegate:(id<NodeCollectionViewCellDelegate> _Nullable)delegate;
- (void)configureCellForFolderLinkNode:(MEGANode *)node allowedMultipleSelection:(BOOL)multipleSelection sdk:(MEGASdk *)sdk delegate:(id<NodeCollectionViewCellDelegate> _Nullable)delegate;
- (void)configureCellForOfflineItem:(NSDictionary *)item itemPath:(NSString *)pathForItem allowedMultipleSelection:(BOOL)multipleSelection sdk:(MEGASdk *)sdk delegate:(id<NodeCollectionViewCellDelegate> _Nullable)delegate;
- (void)setupAppearance;
Expand Down
Loading

0 comments on commit b6545c3

Please sign in to comment.