Add inline audio player in voice messages.

Signed-off-by: Ivan Sein <ivan@nextcloud.com>
This commit is contained in:
Ivan Sein 2021-06-14 17:19:58 +02:00
Родитель 9c4d14a1d7
Коммит 189fec1fcb
15 изменённых файлов: 636 добавлений и 4 удалений

Просмотреть файл

@ -188,6 +188,7 @@
2CA1CCDB1F1F6FCA002FE6A2 /* RoomTableViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 2CA1CCD91F1F6FCA002FE6A2 /* RoomTableViewCell.m */; };
2CA52ACB2670D02800619610 /* VoiceMessageRecordingView.m in Sources */ = {isa = PBXBuildFile; fileRef = 2CA52ACA2670D02800619610 /* VoiceMessageRecordingView.m */; };
2CA52ACD2670D07900619610 /* VoiceMessageRecordingView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 2CA52ACC2670D07900619610 /* VoiceMessageRecordingView.xib */; };
2CA52AD0267613CB00619610 /* VoiceMessageTableViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 2CA52ACF267613CA00619610 /* VoiceMessageTableViewCell.m */; };
2CB304192264775E0053078A /* SLKInputAccessoryView.m in Sources */ = {isa = PBXBuildFile; fileRef = 2CB3039D2264775E0053078A /* SLKInputAccessoryView.m */; };
2CB3041A2264775E0053078A /* SLKTextInput+Implementation.m in Sources */ = {isa = PBXBuildFile; fileRef = 2CB3039E2264775E0053078A /* SLKTextInput+Implementation.m */; };
2CB3041B2264775E0053078A /* SLKTextInputbar.m in Sources */ = {isa = PBXBuildFile; fileRef = 2CB303A12264775E0053078A /* SLKTextInputbar.m */; };
@ -610,6 +611,8 @@
2CA52AC92670D02800619610 /* VoiceMessageRecordingView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = VoiceMessageRecordingView.h; sourceTree = "<group>"; };
2CA52ACA2670D02800619610 /* VoiceMessageRecordingView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = VoiceMessageRecordingView.m; sourceTree = "<group>"; };
2CA52ACC2670D07900619610 /* VoiceMessageRecordingView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = VoiceMessageRecordingView.xib; sourceTree = "<group>"; };
2CA52ACE267613CA00619610 /* VoiceMessageTableViewCell.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = VoiceMessageTableViewCell.h; sourceTree = "<group>"; };
2CA52ACF267613CA00619610 /* VoiceMessageTableViewCell.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = VoiceMessageTableViewCell.m; sourceTree = "<group>"; };
2CA80EDB256C1249006BA449 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Localizable.strings; sourceTree = "<group>"; };
2CA80EDC256C1249006BA449 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/InfoPlist.strings; sourceTree = "<group>"; };
2CA80EDD256C1296006BA449 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = "<group>"; };
@ -1102,6 +1105,8 @@
2C604BD8211988A700D34DCD /* SystemMessageTableViewCell.m */,
2C8E2A19232174C20022BFC9 /* MessageSeparatorTableViewCell.h */,
2C8E2A1A232174C20022BFC9 /* MessageSeparatorTableViewCell.m */,
2CA52ACE267613CA00619610 /* VoiceMessageTableViewCell.h */,
2CA52ACF267613CA00619610 /* VoiceMessageTableViewCell.m */,
);
name = "Chat cells";
sourceTree = "<group>";
@ -1776,6 +1781,7 @@
C1292B8C237313590004C3B7 /* CCBKPasscode.m in Sources */,
2C1ABDE5257F883400AEDFB6 /* ABContact.m in Sources */,
2CA1CCDB1F1F6FCA002FE6A2 /* RoomTableViewCell.m in Sources */,
2CA52AD0267613CB00619610 /* VoiceMessageTableViewCell.m in Sources */,
2C1EF36B25505DCE007C9768 /* NCNavigationController.m in Sources */,
2CC007B420D7AE990096D91F /* ResultMultiSelectionTableViewController.m in Sources */,
2CA1CCC31F166CC5002FE6A2 /* NCRoom.m in Sources */,

23
NextcloudTalk/Images.xcassets/pause.imageset/Contents.json поставляемый Normal file
Просмотреть файл

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

Двоичные данные
NextcloudTalk/Images.xcassets/pause.imageset/baseline_pause_black_24pt_1x.png поставляемый Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 83 B

Двоичные данные
NextcloudTalk/Images.xcassets/pause.imageset/baseline_pause_black_24pt_2x.png поставляемый Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 103 B

Двоичные данные
NextcloudTalk/Images.xcassets/pause.imageset/baseline_pause_black_24pt_3x.png поставляемый Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 109 B

23
NextcloudTalk/Images.xcassets/play.imageset/Contents.json поставляемый Normal file
Просмотреть файл

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

Двоичные данные
NextcloudTalk/Images.xcassets/play.imageset/baseline_play_arrow_black_24pt_1x.png поставляемый Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 144 B

Двоичные данные
NextcloudTalk/Images.xcassets/play.imageset/baseline_play_arrow_black_24pt_2x.png поставляемый Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 212 B

Двоичные данные
NextcloudTalk/Images.xcassets/play.imageset/baseline_play_arrow_black_24pt_3x.png поставляемый Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 265 B

Просмотреть файл

@ -42,6 +42,7 @@ extern NSString * const NCChatFileControllerDidChangeDownloadProgressNotificatio
@interface NCChatFileController : NSObject
@property (nonatomic, weak) id<NCChatFileControllerDelegate> delegate;
@property (nonatomic, strong) NSString *messageType;
- (void)downloadFileFromMessage:(NCMessageFileParameter *)fileParameter;
- (void)downloadFileWithFileId:(NSString *)fileId;

Просмотреть файл

@ -35,6 +35,7 @@ extern NSString * const kMessageTypeComment;
extern NSString * const kMessageTypeCommentDeleted;
extern NSString * const kMessageTypeSystem;
extern NSString * const kMessageTypeCommand;
extern NSString * const kMessageTypeVoiceMessage;
@interface NCChatMessage : RLMObject <NSCopying>

Просмотреть файл

@ -31,6 +31,7 @@ NSString * const kMessageTypeComment = @"comment";
NSString * const kMessageTypeCommentDeleted = @"comment_deleted";
NSString * const kMessageTypeSystem = @"system";
NSString * const kMessageTypeCommand = @"command";
NSString * const kMessageTypeVoiceMessage = @"voice-message";
@interface NCChatMessage ()
{

Просмотреть файл

@ -70,6 +70,7 @@
#import "SystemMessageTableViewCell.h"
#import "ShareLocationViewController.h"
#import "VoiceMessageRecordingView.h"
#import "VoiceMessageTableViewCell.h"
#define k_send_message_button_tag 99
@ -84,7 +85,7 @@ typedef enum NCChatMessageAction {
kNCChatMessageActionOpenFileInNextcloud
} NCChatMessageAction;
@interface NCChatViewController () <UIGestureRecognizerDelegate, UINavigationControllerDelegate, UIImagePickerControllerDelegate, UIDocumentPickerDelegate, ShareConfirmationViewControllerDelegate, FileMessageTableViewCellDelegate, NCChatFileControllerDelegate, QLPreviewControllerDelegate, QLPreviewControllerDataSource, ChatMessageTableViewCellDelegate, ShareLocationViewControllerDelegate, LocationMessageTableViewCellDelegate, AVAudioRecorderDelegate>
@interface NCChatViewController () <UIGestureRecognizerDelegate, UINavigationControllerDelegate, UIImagePickerControllerDelegate, UIDocumentPickerDelegate, ShareConfirmationViewControllerDelegate, FileMessageTableViewCellDelegate, NCChatFileControllerDelegate, QLPreviewControllerDelegate, QLPreviewControllerDataSource, ChatMessageTableViewCellDelegate, ShareLocationViewControllerDelegate, LocationMessageTableViewCellDelegate, VoiceMessageTableViewCellDelegate, AVAudioRecorderDelegate, AVAudioPlayerDelegate>
@property (nonatomic, strong) NCChatController *chatController;
@property (nonatomic, strong) NCChatTitleView *titleView;
@ -125,6 +126,9 @@ typedef enum NCChatMessageAction {
@property (nonatomic, assign) CGPoint longPressStartingPoint;
@property (nonatomic, assign) CGFloat cancelHintLabelInitialPositionX;
@property (nonatomic, assign) BOOL recordCancelled;
@property (nonatomic, strong) AVAudioPlayer *voiceMessagesPlayer;
@property (nonatomic, strong) NSTimer *playerProgressTimer;
@property (nonatomic, strong) NCChatFileStatus *playerAudioFileStatus;
@end
@ -292,6 +296,8 @@ NSString * const NCChatViewControllerReplyPrivatelyNotification = @"NCChatViewCo
[self.tableView registerClass:[LocationMessageTableViewCell class] forCellReuseIdentifier:GroupedLocationMessageCellIdentifier];
[self.tableView registerClass:[SystemMessageTableViewCell class] forCellReuseIdentifier:SystemMessageCellIdentifier];
[self.tableView registerClass:[SystemMessageTableViewCell class] forCellReuseIdentifier:InvisibleSystemMessageCellIdentifier];
[self.tableView registerClass:[VoiceMessageTableViewCell class] forCellReuseIdentifier:VoiceMessageCellIdentifier];
[self.tableView registerClass:[VoiceMessageTableViewCell class] forCellReuseIdentifier:GroupedVoiceMessageCellIdentifier];
[self.tableView registerClass:[MessageSeparatorTableViewCell class] forCellReuseIdentifier:MessageSeparatorCellIdentifier];
[self.autoCompletionView registerClass:[ChatMessageTableViewCell class] forCellReuseIdentifier:AutoCompletionCellIdentifier];
[self registerPrefixesForAutoCompletion:@[@"@"]];
@ -1446,13 +1452,77 @@ NSString * const NCChatViewControllerReplyPrivatelyNotification = @"NCChatViewCo
#pragma mark - AVAudioRecorderDelegate
- (void) audioRecorderDidFinishRecording:(AVAudioRecorder *)recorder successfully:(BOOL)flag
- (void)audioRecorderDidFinishRecording:(AVAudioRecorder *)recorder successfully:(BOOL)flag
{
if (flag && recorder == _recorder && !_recordCancelled) {
[self shareVoiceMessage];
}
}
#pragma mark - Voice Messages Player
- (void)setupVoiceMessagePlayerWithAudioFileStatus:(NCChatFileStatus *)fileStatus
{
NSData *data = [NSData dataWithContentsOfFile:fileStatus.fileLocalPath];
NSError *error;
_voiceMessagesPlayer = [[AVAudioPlayer alloc] initWithData:data error:&error];
if (!error) {
_playerAudioFileStatus = fileStatus;
[self playVoiceMessagePlayer];
} else {
NSLog(@"Error: %@", error);
}
}
- (void)playVoiceMessagePlayer
{
[self startVoiceMessagePlayerTimer];
[_voiceMessagesPlayer play];
}
- (void)pauseVoiceMessagePlayer
{
[self stopVoiceMessagePlayerTimer];
[_voiceMessagesPlayer pause];
[self checkVisibleCellAudioPlayers];
}
- (void)checkVisibleCellAudioPlayers
{
for (NSIndexPath *indexPath in self.tableView.indexPathsForVisibleRows) {
NSDate *sectionDate = [_dateSections objectAtIndex:indexPath.section];
NCChatMessage *message = [[_messages objectForKey:sectionDate] objectAtIndex:indexPath.row];
if ([message.messageType isEqualToString:kMessageTypeVoiceMessage]) {
VoiceMessageTableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
if (message.file && [message.file.parameterId isEqualToString:_playerAudioFileStatus.fileId] && [message.file.path isEqualToString:_playerAudioFileStatus.filePath]) {
[cell setPlayerProgress:_voiceMessagesPlayer.currentTime/_voiceMessagesPlayer.duration isPlaying:_voiceMessagesPlayer.isPlaying];
continue;
}
[cell resetPlayer];
}
}
}
- (void)startVoiceMessagePlayerTimer
{
[self stopVoiceMessagePlayerTimer];
_playerProgressTimer = [NSTimer scheduledTimerWithTimeInterval:0.05 target:self selector:@selector(checkVisibleCellAudioPlayers) userInfo:nil repeats:YES];
}
- (void)stopVoiceMessagePlayerTimer
{
[_playerProgressTimer invalidate];
_playerProgressTimer = nil;
}
#pragma mark - AVAudioPlayerDelegate
- (void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)player successfully:(BOOL)flag
{
[self stopVoiceMessagePlayerTimer];
[self checkVisibleCellAudioPlayers];
}
#pragma mark - Gesture recognizer
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
@ -2579,6 +2649,14 @@ NSString * const NCChatViewControllerReplyPrivatelyNotification = @"NCChatViewCo
return systemCell;
}
if (message.file) {
if ([message.messageType isEqualToString:kMessageTypeVoiceMessage]) {
NSString *voiceMessageCellIdentifier = (message.isGroupMessage) ? GroupedVoiceMessageCellIdentifier : VoiceMessageCellIdentifier;
VoiceMessageTableViewCell *voiceMessageCell = (VoiceMessageTableViewCell *)[self.tableView dequeueReusableCellWithIdentifier:voiceMessageCellIdentifier];
voiceMessageCell.delegate = self;
[voiceMessageCell setupForMessage:message withLastCommonReadMessage:_room.lastCommonReadMessage];
return voiceMessageCell;
}
NSString *fileCellIdentifier = (message.isGroupMessage) ? GroupedFileMessageCellIdentifier : FileMessageCellIdentifier;
FileMessageTableViewCell *fileCell = (FileMessageTableViewCell *)[self.tableView dequeueReusableCellWithIdentifier:fileCellIdentifier];
fileCell.delegate = self;
@ -2684,12 +2762,18 @@ NSString * const NCChatViewControllerReplyPrivatelyNotification = @"NCChatViewCo
}
}
// Voice message should be before message.file check since it contains a file
if ([message.messageType isEqualToString:kMessageTypeVoiceMessage]) {
height -= CGRectGetHeight(bodyBounds);
return height += kVoiceMessageCellPlayerHeight;
}
if (message.file) {
height += kFileMessageCellFilePreviewHeight + 15;
return height += kFileMessageCellFilePreviewHeight + 15;
}
if (message.geoLocation) {
height += kLocationMessageCellPreviewHeight + 15;
return height += kLocationMessageCellPreviewHeight + 15;
}
return height;
@ -2871,6 +2955,33 @@ NSString * const NCChatViewControllerReplyPrivatelyNotification = @"NCChatViewCo
[downloader downloadFileFromMessage:fileParameter];
}
#pragma mark - VoiceMessageTableViewCellDelegate
- (void)cellWantsToPlayAudioFile:(NCMessageFileParameter *)fileParameter
{
if (fileParameter.fileStatus && fileParameter.fileStatus.isDownloading) {
NSLog(@"File already downloading -> skipping new download");
return;
}
if (!_voiceMessagesPlayer.isPlaying && [fileParameter.parameterId isEqualToString:_playerAudioFileStatus.fileId] && [fileParameter.path isEqualToString:_playerAudioFileStatus.filePath]) {
[self playVoiceMessagePlayer];
return;
}
NCChatFileController *downloader = [[NCChatFileController alloc] init];
downloader.delegate = self;
downloader.messageType = kMessageTypeVoiceMessage;
[downloader downloadFileFromMessage:fileParameter];
}
- (void)cellWantsToPauseAudioFile:(NCMessageFileParameter *)fileParameter
{
if (_voiceMessagesPlayer.isPlaying && [fileParameter.parameterId isEqualToString:_playerAudioFileStatus.fileId] && [fileParameter.path isEqualToString:_playerAudioFileStatus.filePath]) {
[self pauseVoiceMessagePlayer];
}
}
#pragma mark - LocationMessageTableViewCellDelegate
- (void)cellWantsToOpenLocation:(GeoLocationRichObject *)geoLocationRichObject
@ -2897,6 +3008,11 @@ NSString * const NCChatViewControllerReplyPrivatelyNotification = @"NCChatViewCo
- (void)fileControllerDidLoadFile:(NCChatFileController *)fileController withFileStatus:(NCChatFileStatus *)fileStatus
{
if ([fileController.messageType isEqualToString:kMessageTypeVoiceMessage]) {
[self setupVoiceMessagePlayerWithAudioFileStatus:fileStatus];
return;
}
if (_isPreviewControllerShown) {
// We are showing a file already, no need to open another one
return;

Просмотреть файл

@ -0,0 +1,63 @@
/**
* @copyright Copyright (c) 2020 Ivan Sein <ivan@nextcloud.com>
*
* @author Ivan Sein <ivan@nextcloud.com>
*
* @license GNU GPL version 3 or any later version
*
* 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 <UIKit/UIKit.h>
#import "ChatTableViewCell.h"
#import "MessageBodyTextView.h"
#import "NCMessageFileParameter.h"
#import "NCChatMessage.h"
static CGFloat kVoiceMessageCellMinimumHeight = 50.0;
static CGFloat kVoiceMessageCellAvatarHeight = 30.0;
static CGFloat kVoiceMessageCellPlayerHeight = 44.0;
static NSString *VoiceMessageCellIdentifier = @"VoiceMessageCellIdentifier";
static NSString *GroupedVoiceMessageCellIdentifier = @"GroupedVoiceMessageCellIdentifier";
@protocol VoiceMessageTableViewCellDelegate <NSObject>
- (void)cellWantsToPlayAudioFile:(NCMessageFileParameter *)fileParameter;
- (void)cellWantsToPauseAudioFile:(NCMessageFileParameter *)fileParameter;
@end
@interface VoiceMessageTableViewCell : ChatTableViewCell
@property (nonatomic, weak) id<VoiceMessageTableViewCellDelegate> delegate;
@property (nonatomic, strong) UILabel *titleLabel;
@property (nonatomic, strong) UILabel *dateLabel;
@property (nonatomic, strong) MessageBodyTextView *bodyTextView;
@property (nonatomic, strong) UIImageView *avatarView;
@property (nonatomic, strong) UIView *statusView;
@property (nonatomic, strong) UIView *fileStatusView;
@property (nonatomic, strong) NCMessageFileParameter *fileParameter;
@property (nonatomic, strong) UIButton *playPauseButton;
@property (nonatomic, strong) UIProgressView *progressView;
+ (CGFloat)defaultFontSize;
- (void)setGuestAvatar:(NSString *)displayName;
- (void)setupForMessage:(NCChatMessage *)message withLastCommonReadMessage:(NSInteger)lastCommonRead;
- (void)setPlayerProgress:(CGFloat)progress isPlaying:(BOOL)playing;
- (void)resetPlayer;
@end

Просмотреть файл

@ -0,0 +1,398 @@
/**
* @copyright Copyright (c) 2020 Ivan Sein <ivan@nextcloud.com>
*
* @author Ivan Sein <ivan@nextcloud.com>
*
* @license GNU GPL version 3 or any later version
*
* 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 "VoiceMessageTableViewCell.h"
#import "MaterialActivityIndicator.h"
#import "SLKUIConstants.h"
#import "UIImageView+AFNetworking.h"
#import "UIImageView+Letters.h"
#import "NCAPIController.h"
#import "NCAppBranding.h"
#import "NCChatFileController.h"
#import "NCDatabaseManager.h"
#import "NCUtils.h"
#define k_play_button_tag 99
#define k_pause_button_tag 98
@interface VoiceMessageTableViewCell ()
{
MDCActivityIndicator *_activityIndicator;
}
@end
@implementation VoiceMessageTableViewCell
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier
{
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if (self) {
self.backgroundColor = [NCAppBranding backgroundColor];
[self configureSubviews];
}
return self;
}
- (void)configureSubviews
{
_avatarView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, kVoiceMessageCellAvatarHeight, kVoiceMessageCellAvatarHeight)];
_avatarView.translatesAutoresizingMaskIntoConstraints = NO;
_avatarView.userInteractionEnabled = NO;
_avatarView.backgroundColor = [NCAppBranding placeholderColor];
_avatarView.layer.cornerRadius = kVoiceMessageCellAvatarHeight/2.0;
_avatarView.layer.masksToBounds = YES;
if ([self.reuseIdentifier isEqualToString:VoiceMessageCellIdentifier]) {
[self.contentView addSubview:_avatarView];
[self.contentView addSubview:self.titleLabel];
[self.contentView addSubview:self.dateLabel];
}
[self.contentView addSubview:self.bodyTextView];
self.playPauseButton = [UIButton buttonWithType:UIButtonTypeCustom];
self.playPauseButton.translatesAutoresizingMaskIntoConstraints = NO;
[self.playPauseButton addTarget:self action:@selector(playPauseButtonTapped:) forControlEvents:UIControlEventTouchUpInside];
[self setPlayButton];
[self.contentView addSubview:self.playPauseButton];
self.progressView = [[UIProgressView alloc] initWithFrame:CGRectMake(0, 0, 200, 44)];
self.progressView.translatesAutoresizingMaskIntoConstraints = NO;
[self.contentView addSubview:self.progressView];
_statusView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, kChatCellStatusViewHeight, kChatCellStatusViewHeight)];
_statusView.translatesAutoresizingMaskIntoConstraints = NO;
// [_statusView setBackgroundColor:[UIColor greenColor]];
[self.contentView addSubview:_statusView];
_fileStatusView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, kChatCellStatusViewHeight, kChatCellStatusViewHeight)];
_fileStatusView.translatesAutoresizingMaskIntoConstraints = NO;
// [_fileStatusView setBackgroundColor:[UIColor purpleColor]];
[self.contentView addSubview:_fileStatusView];
NSDictionary *views = @{@"avatarView": self.avatarView,
@"statusView": self.statusView,
@"fileStatusView": self.fileStatusView,
@"playButton" : self.playPauseButton,
@"progressView" : self.progressView,
@"titleLabel": self.titleLabel,
@"dateLabel": self.dateLabel,
@"bodyTextView": self.bodyTextView,
};
NSDictionary *metrics = @{@"avatarSize": @(kVoiceMessageCellAvatarHeight),
@"statusSize": @(kChatCellStatusViewHeight),
@"statusTopPadding": @17,
@"buttonHeight": @44,
@"progressWidth": @200,
@"progressTopPadding": @25,
@"progressBottomPadding": @30,
@"progressHeight": @4,
@"padding": @15,
@"avatarGap": @50,
@"right": @10,
@"left": @5
};
if ([self.reuseIdentifier isEqualToString:VoiceMessageCellIdentifier]) {
[self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-right-[avatarView(avatarSize)]-right-[titleLabel]-[dateLabel(40)]-right-|" options:0 metrics:metrics views:views]];
[self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-avatarGap-[playButton(buttonHeight)]-[progressView(progressWidth)]-[fileStatusView(statusSize)]-(>=0)-|" options:0 metrics:metrics views:views]];
[self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-padding-[statusView(statusSize)]-padding-[bodyTextView(>=0)]-right-|" options:0 metrics:metrics views:views]];
[self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-right-[titleLabel(28)]-left-[playButton(buttonHeight)]-right-[bodyTextView(>=0@999)]-left-|" options:0 metrics:metrics views:views]];
[self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-right-[titleLabel(28)]-progressTopPadding-[progressView(progressHeight)]-progressBottomPadding-[bodyTextView(>=0@999)]-left-|" options:0 metrics:metrics views:views]];
[self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-right-[dateLabel(28)]-left-[playButton(buttonHeight)]-right-[bodyTextView(>=0@999)]-left-|" options:0 metrics:metrics views:views]];
[self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-right-[avatarView(avatarSize)]-(>=0)-|" options:0 metrics:metrics views:views]];
[self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-right-[titleLabel(28)]-statusTopPadding-[fileStatusView(statusSize)]-(>=0)-|" options:0 metrics:metrics views:views]];
[self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-right-[titleLabel(28)]-statusTopPadding-[statusView(statusSize)]-(>=0)-|" options:0 metrics:metrics views:views]];
} else if ([self.reuseIdentifier isEqualToString:GroupedVoiceMessageCellIdentifier]) {
[self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-avatarGap-[playButton(buttonHeight)]-[progressView(progressWidth)]-[fileStatusView(statusSize)]-(>=0)-|" options:0 metrics:metrics views:views]];
[self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-padding-[statusView(statusSize)]-padding-[bodyTextView(>=0)]-right-|" options:0 metrics:metrics views:views]];
[self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-left-[playButton(buttonHeight)]-right-[bodyTextView(>=0@999)]-left-|" options:0 metrics:metrics views:views]];
[self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-progressTopPadding-[progressView(progressHeight)]-progressBottomPadding-[bodyTextView(>=0@999)]-left-|" options:0 metrics:metrics views:views]];
[self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-statusTopPadding-[fileStatusView(statusSize)]-(>=0)-|" options:0 metrics:metrics views:views]];
[self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-statusTopPadding-[statusView(statusSize)]-(>=0)-|" options:0 metrics:metrics views:views]];
}
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didChangeIsDownloading:) name:NCChatFileControllerDidChangeIsDownloadingNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didChangeDownloadProgress:) name:NCChatFileControllerDidChangeDownloadProgressNotification object:nil];
}
- (void)prepareForReuse
{
[super prepareForReuse];
CGFloat pointSize = [VoiceMessageTableViewCell defaultFontSize];
self.titleLabel.font = [UIFont systemFontOfSize:pointSize];
self.bodyTextView.font = [UIFont systemFontOfSize:pointSize];
self.titleLabel.text = @"";
self.bodyTextView.text = @"";
self.dateLabel.text = @"";
[self.avatarView cancelImageDownloadTask];
self.avatarView.image = nil;
[self.statusView.subviews makeObjectsPerformSelector: @selector(removeFromSuperview)];
[self clearFileStatusView];
}
- (void)setupForMessage:(NCChatMessage *)message withLastCommonReadMessage:(NSInteger)lastCommonRead
{
self.titleLabel.text = message.actorDisplayName;
self.messageId = message.messageId;
NSDate *date = [[NSDate alloc] initWithTimeIntervalSince1970:message.timestamp];
self.dateLabel.text = [NCUtils getTimeFromDate:date];
TalkAccount *activeAccount = [[NCDatabaseManager sharedInstance] activeAccount];
[self.avatarView setImageWithURLRequest:[[NCAPIController sharedInstance] createAvatarRequestForUser:message.actorId andSize:96 usingAccount:activeAccount]
placeholderImage:nil success:nil failure:nil];
if (message.sendingFailed) {
UIImageView *errorView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 20, 20)];
[errorView setImage:[UIImage imageNamed:@"error"]];
[self.statusView addSubview:errorView];
} else if (message.isTemporary) {
[self addActivityIndicator:0];
} else if (message.file.fileStatus) {
if (message.file.fileStatus.isDownloading && message.file.fileStatus.downloadProgress < 1) {
[self addActivityIndicator:message.file.fileStatus.downloadProgress];
}
}
self.fileParameter = message.file;
ServerCapabilities *serverCapabilities = [[NCDatabaseManager sharedInstance] serverCapabilitiesForAccountId:activeAccount.accountId];
BOOL shouldShowDeliveryStatus = [[NCDatabaseManager sharedInstance] serverHasTalkCapability:kCapabilityChatReadStatus forAccountId:activeAccount.accountId];
BOOL shouldShowReadStatus = !serverCapabilities.readStatusPrivacy;
if ([message.actorId isEqualToString:activeAccount.userId] && [message.actorType isEqualToString:@"users"] && shouldShowDeliveryStatus) {
if (lastCommonRead >= message.messageId && shouldShowReadStatus) {
[self setDeliveryState:ChatMessageDeliveryStateRead];
} else {
[self setDeliveryState:ChatMessageDeliveryStateSent];
}
}
}
- (void)setDeliveryState:(ChatMessageDeliveryState)state
{
[self.statusView.subviews makeObjectsPerformSelector: @selector(removeFromSuperview)];
if (state == ChatMessageDeliveryStateSent) {
UIImageView *checkView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 20, 20)];
[checkView setImage:[UIImage imageNamed:@"check"]];
checkView.image = [checkView.image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
[checkView setTintColor:[UIColor lightGrayColor]];
[self.statusView addSubview:checkView];
} else if (state == ChatMessageDeliveryStateRead) {
UIImageView *checkAllView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 20, 20)];
[checkAllView setImage:[UIImage imageNamed:@"check-all"]];
checkAllView.image = [checkAllView.image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
[checkAllView setTintColor:[UIColor lightGrayColor]];
[self.statusView addSubview:checkAllView];
}
}
- (void)setPlayerProgress:(CGFloat)progress isPlaying:(BOOL)playing
{
[self setPauseButton];
if (!playing) {
[self setPlayButton];
}
[self.progressView setProgress:progress];
}
- (void)resetPlayer
{
[self setPlayButton];
[self.progressView setProgress:0];
}
- (void)setPlayButton
{
UIImage *image = [[UIImage imageNamed:@"play"] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
[self.playPauseButton setImage:image forState:UIControlStateNormal];
self.playPauseButton.tag = k_play_button_tag;
}
- (void)setPauseButton
{
UIImage *image = [[UIImage imageNamed:@"pause"] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
[self.playPauseButton setImage:image forState:UIControlStateNormal];
self.playPauseButton.tag = k_pause_button_tag;
}
- (void)didChangeIsDownloading:(NSNotification *)notification
{
dispatch_async(dispatch_get_main_queue(), ^{
NCChatFileStatus *receivedStatus = [notification.userInfo objectForKey:@"fileStatus"];
if (![receivedStatus.fileId isEqualToString:self->_fileParameter.parameterId] || ![receivedStatus.filePath isEqualToString:self->_fileParameter.path]) {
// Received a notification for a different cell
return;
}
BOOL isDownloading = [[notification.userInfo objectForKey:@"isDownloading"] boolValue];
if (isDownloading && !self->_activityIndicator) {
// Immediately show an indeterminate indicator as long as we don't have a progress value
[self addActivityIndicator:0];
} else if (!isDownloading && self->_activityIndicator) {
[self clearFileStatusView];
}
});
}
- (void)didChangeDownloadProgress:(NSNotification *)notification
{
dispatch_async(dispatch_get_main_queue(), ^{
NCChatFileStatus *receivedStatus = [notification.userInfo objectForKey:@"fileStatus"];
if (![receivedStatus.fileId isEqualToString:self->_fileParameter.parameterId] || ![receivedStatus.filePath isEqualToString:self->_fileParameter.path]) {
// Received a notification for a different cell
return;
}
double progress = [[notification.userInfo objectForKey:@"progress"] doubleValue];
if (self->_activityIndicator) {
// Switch to determinate-mode and show progress
self->_activityIndicator.indicatorMode = MDCActivityIndicatorModeDeterminate;
[self->_activityIndicator setProgress:progress animated:YES];
} else {
// Make sure we have an activity indicator added to this cell
[self addActivityIndicator:progress];
}
});
}
- (void)addActivityIndicator:(CGFloat)progress
{
[self clearFileStatusView];
_activityIndicator = [[MDCActivityIndicator alloc] initWithFrame:CGRectMake(0, 0, 20, 20)];
_activityIndicator.radius = 7.0f;
_activityIndicator.cycleColors = @[UIColor.lightGrayColor];
if (progress > 0) {
_activityIndicator.indicatorMode = MDCActivityIndicatorModeDeterminate;
[_activityIndicator setProgress:progress animated:NO];
}
[_activityIndicator startAnimating];
[self.fileStatusView addSubview:_activityIndicator];
}
#pragma mark - Getters
- (UILabel *)titleLabel
{
if (!_titleLabel) {
_titleLabel = [UILabel new];
_titleLabel.translatesAutoresizingMaskIntoConstraints = NO;
_titleLabel.backgroundColor = [UIColor clearColor];
_titleLabel.userInteractionEnabled = NO;
_titleLabel.numberOfLines = 0;
_titleLabel.textColor = [UIColor lightGrayColor];
_titleLabel.font = [UIFont systemFontOfSize:[VoiceMessageTableViewCell defaultFontSize]];
if (@available(iOS 13.0, *)) {
_titleLabel.textColor = [UIColor secondaryLabelColor];
}
}
return _titleLabel;
}
- (UILabel *)dateLabel
{
if (!_dateLabel) {
_dateLabel = [UILabel new];
_dateLabel.textAlignment = NSTextAlignmentRight;
_dateLabel.translatesAutoresizingMaskIntoConstraints = NO;
_dateLabel.backgroundColor = [UIColor clearColor];
_dateLabel.userInteractionEnabled = NO;
_dateLabel.numberOfLines = 0;
_dateLabel.textColor = [UIColor lightGrayColor];
_dateLabel.font = [UIFont systemFontOfSize:12.0];
if (@available(iOS 13.0, *)) {
_dateLabel.textColor = [UIColor secondaryLabelColor];
}
}
return _dateLabel;
}
- (MessageBodyTextView *)bodyTextView
{
if (!_bodyTextView) {
_bodyTextView = [MessageBodyTextView new];
_bodyTextView.font = [UIFont systemFontOfSize:[VoiceMessageTableViewCell defaultFontSize]];
_bodyTextView.dataDetectorTypes = UIDataDetectorTypeNone;
}
return _bodyTextView;
}
- (void)playPauseButtonTapped:(id)sender
{
if (!self.fileParameter || !self.fileParameter.path || !self.fileParameter.link) {
return;
}
if (self.delegate) {
UIButton *buttton = sender;
if (buttton.tag == k_play_button_tag) {
[self.delegate cellWantsToPlayAudioFile:self.fileParameter];
} else if (buttton.tag == k_pause_button_tag) {
[self.delegate cellWantsToPauseAudioFile:self.fileParameter];
}
}
}
- (void)setGuestAvatar:(NSString *)displayName
{
UIColor *guestAvatarColor = [NCAppBranding placeholderColor];
NSString *name = ([displayName isEqualToString:@""]) ? @"?" : displayName;
[_avatarView setImageWithString:name color:guestAvatarColor circular:true];
}
- (void)clearFileStatusView {
if (_activityIndicator) {
[_activityIndicator stopAnimating];
_activityIndicator = nil;
}
[self.fileStatusView.subviews makeObjectsPerformSelector: @selector(removeFromSuperview)];
}
+ (CGFloat)defaultFontSize
{
CGFloat pointSize = 16.0;
// NSString *contentSizeCategory = [[UIApplication sharedApplication] preferredContentSizeCategory];
// pointSize += SLKPointSizeDifferenceForCategory(contentSizeCategory);
return pointSize;
}
@end