9 |
10 | @interface MUAudioBarView () {
11 | CGFloat _below;
12 | CGFloat _above;
13 | CGFloat _min;
14 | CGFloat _max;
15 | CGFloat _value;
16 | NSTimer *_timer;
17 | }
18 | @end
19 |
20 | @implementation MUAudioBarView
21 |
22 | - (id) initWithFrame:(CGRect)frame {
23 | if ((self = [super initWithFrame:frame])) {
24 | _value = 0.5f;
25 | _min = 0.0f;
26 | _max = 1.0f;
27 | _timer = [NSTimer timerWithTimeInterval:1/60.0f target:self selector:@selector(tickTock) userInfo:nil repeats:YES];
28 | [[NSRunLoop mainRunLoop] addTimer:_timer forMode:NSRunLoopCommonModes];
29 | }
30 | return self;
31 | }
32 |
33 | - (void) dealloc {
34 | [_timer invalidate];
35 | }
36 |
37 | - (void) setBelow:(CGFloat)below {
38 | _below = below;
39 | }
40 |
41 | - (void) setAbove:(CGFloat)above {
42 | _above = above;
43 | }
44 |
45 | - (void) drawRect:(CGRect)rect {
46 | CGRect bounds = self.bounds;
47 | CGContextRef ctx = UIGraphicsGetCurrentContext();
48 | CGContextClearRect(ctx, bounds);
49 |
50 | _below = [[NSUserDefaults standardUserDefaults] floatForKey:@"AudioVADBelow"];
51 | _above = [[NSUserDefaults standardUserDefaults] floatForKey:@"AudioVADAbove"];
52 |
53 | CGFloat scale = bounds.size.width / (_max - _min);
54 | int below = (int)((_below-_min)*scale);
55 | int above = (int)((_above-_min)*scale);
56 | int value = (int)((_value-_min)*scale);
57 |
58 | CGColorRef redA = [[MUColor badPingColor] colorWithAlphaComponent:0.6f].CGColor;
59 | CGColorRef redO = [MUColor badPingColor].CGColor;
60 | CGColorRef yellowA = [[MUColor mediumPingColor] colorWithAlphaComponent:0.6f].CGColor;
61 | CGColorRef yellowO = [MUColor mediumPingColor].CGColor;
62 | CGColorRef greenA = [[MUColor goodPingColor] colorWithAlphaComponent:0.6f].CGColor;
63 | CGColorRef greenO = [MUColor goodPingColor].CGColor;
64 |
65 | if (_above < _below) {
66 | CGContextSetFillColorWithColor(ctx, redA);
67 | CGContextFillRect(ctx, bounds);
68 | return;
69 | }
70 |
71 | CGRect redBounds = CGRectMake(bounds.origin.x, 0, below, bounds.size.height);
72 | CGContextSetFillColorWithColor(ctx, redA);
73 | CGContextFillRect(ctx, redBounds);
74 |
75 | int x = redBounds.size.width;
76 | CGRect yellowBounds = CGRectMake(x, 0, above-x, bounds.size.height);
77 | CGContextSetFillColorWithColor(ctx, yellowA);
78 | CGContextFillRect(ctx, yellowBounds);
79 |
80 | x = yellowBounds.origin.x+yellowBounds.size.width;
81 | CGRect greenBounds = CGRectMake(x, 0, bounds.size.width-x, bounds.size.height);
82 | CGContextSetFillColorWithColor(ctx, greenA);
83 | CGContextFillRect(ctx, greenBounds);
84 |
85 | if (value > below) {
86 | CGContextSetFillColorWithColor(ctx, redO);
87 | CGContextFillRect(ctx, redBounds);
88 | } else {
89 | redBounds = CGRectMake(bounds.origin.x, 0, value, bounds.size.height);
90 | CGContextSetFillColorWithColor(ctx, redO);
91 | CGContextFillRect(ctx, redBounds);
92 | }
93 | if (value > above) {
94 | CGContextSetFillColorWithColor(ctx, yellowO);
95 | CGContextFillRect(ctx, yellowBounds);
96 |
97 | greenBounds = CGRectMake(x, 0, value-x, bounds.size.height);
98 | CGContextSetFillColorWithColor(ctx, greenO);
99 | CGContextFillRect(ctx, greenBounds);
100 | } else if (value > below && value <= above) {
101 | x = redBounds.size.width;
102 | CGRect yellowBounds = CGRectMake(x, 0, value-x, bounds.size.height);
103 | CGContextSetFillColorWithColor(ctx, yellowO);
104 | CGContextFillRect(ctx, yellowBounds);
105 | }
106 | }
107 |
108 | - (void) tickTock {
109 | MKAudio *audio = [MKAudio sharedAudio];
110 | NSString *kind = [[NSUserDefaults standardUserDefaults] objectForKey:@"AudioVADKind"];
111 | if (![[NSUserDefaults standardUserDefaults] boolForKey:@"AudioPreprocessor"])
112 | kind = @"amplitude";
113 | if ([kind isEqualToString:@"snr"]) {
114 | _value = [audio speechProbablity];
115 | } else {
116 | _value = ([audio peakCleanMic] + 96.0)/96.0;
117 | }
118 | [self setNeedsDisplay];
119 | }
120 |
121 | @end
122 |
--------------------------------------------------------------------------------
/Tests/MUTextMessageProcessorTest.m:
--------------------------------------------------------------------------------
1 | // Copyright 2013 The 'Mumble for iOS' Developers. All rights reserved.
2 | // Use of this source code is governed by a BSD-style
3 | // license that can be found in the LICENSE file.
4 |
5 | #import "MUTextMessageProcessorTest.h"
6 | #import "MUTextMessageProcessor.h"
7 |
8 | @interface MUTextMessageProcessorTest ()
9 | + (NSString *) plainStringFromLinks:(NSArray *)links;
10 | + (NSString *) htmlStringFromLinks:(NSArray *)links;
11 | @end
12 |
13 | @implementation MUTextMessageProcessorTest
14 |
15 | - (void)setUp {
16 | [super setUp];
17 | }
18 |
19 | - (void)tearDown {
20 | [super tearDown];
21 | }
22 |
23 | - (void) testSingleLink {
24 | NSString *plain = @"Hello there. Here's a link: http://www.google.com";
25 | NSString *expected = @"Hello there. Here's a link: http://www.google.com
";
26 | NSString *html = [MUTextMessageProcessor processedHTMLFromPlainTextMessage:plain];
27 | STAssertEqualObjects(html, expected, nil);
28 | }
29 |
30 | - (void) testSingleLinkWithTrailer {
31 | NSString *plain = @"Hello there. Here's a link: http://www.google.com, and a trailer!";
32 | NSString *expected = @"Hello there. Here's a link: http://www.google.com, and a trailer!
";
33 | NSString *html = [MUTextMessageProcessor processedHTMLFromPlainTextMessage:plain];
34 | STAssertEqualObjects(html, expected, nil);
35 | }
36 |
37 | - (void) testMultiLink {
38 | NSString *plain = @"1st: http://www.a.com, 2nd: http://www.b.com";
39 | NSString *expected = @"1st: http://www.a.com, 2nd: http://www.b.com
";
40 | NSString *html = [MUTextMessageProcessor processedHTMLFromPlainTextMessage:plain];
41 | STAssertEqualObjects(html, expected, nil);
42 | }
43 |
44 | - (void) testPercentEncoding {
45 | NSString *plain = @"Hello there. Here's a link: http://www.example.com/%20a%20lot%20of%20spaces";
46 | NSString *expected = @"Hello there. Here's a link: http://www.example.com/%20a%20lot%20of%20spaces
";
47 | NSString *html = [MUTextMessageProcessor processedHTMLFromPlainTextMessage:plain];
48 | STAssertEqualObjects(html, expected, nil);
49 | }
50 |
51 | + (NSString *) plainStringFromLinks:(NSArray *)links {
52 | NSMutableString *str = [NSMutableString string];
53 | for (NSString *url in links) {
54 | [str appendString:url];
55 | [str appendString:@" "];
56 | }
57 | return str;
58 | }
59 |
60 | + (NSString *) htmlStringFromLinks:(NSArray *)links {
61 | NSMutableString *str = [NSMutableString string];
62 | [str appendString:@""];
63 | for (NSString *url in links) {
64 | [str appendString:@""];
67 | [str appendString:url];
68 | [str appendString:@" "];
69 | }
70 | [str appendString:@"
"];
71 | return str;
72 | }
73 |
74 | - (void) testManyLinks {
75 | NSArray *links = @[
76 | @"http://www.google.com",
77 | @"http://www.facebook.com",
78 | @"https://www.yahoo.com",
79 | @"mumble://test.mumble.info:64738",
80 | @"mumble://test.mumble.info:64738/Test/Some/Channel?version=1.2.0"
81 | @"steam://43145",
82 | @"mailto:test@example.com",
83 | @"file:///Users/luser/Documents/test.html",
84 | @"http://www.google.dk/#hl=en&safe=off&tbo=d&output=search&sclient=psy-ab&q=hello+world&oq=hello+world",
85 | @"http://www.bing.com/%20%20already%20%20percent%20%20encoded",
86 | @"http://www.nonascii.com/æøæåæøæåææøæåæø",
87 | @"http://pctenc.info/%26%25%26%25",
88 | ];
89 | NSString *plain = [MUTextMessageProcessorTest plainStringFromLinks:links];
90 | NSString *expected = [MUTextMessageProcessorTest htmlStringFromLinks:links];
91 | NSString *html = [MUTextMessageProcessor processedHTMLFromPlainTextMessage:plain];
92 | STAssertEqualObjects(html, expected, nil);
93 | }
94 |
95 | - (void) testInvalidURLs {
96 | NSArray *links = @[
97 | @"http://currency.eu/€€€",
98 | @"http://bah/?hello#hey#ho",
99 | ];
100 | for (NSString *url in links) {
101 | NSString *plain = [MUTextMessageProcessorTest plainStringFromLinks:links];
102 | NSString *expected = [MUTextMessageProcessorTest htmlStringFromLinks:links];
103 | NSString *html = [MUTextMessageProcessor processedHTMLFromPlainTextMessage:plain];
104 | STAssertFalse([html isEqualToString:expected], @"got '%@', did not expect '%@'", html, expected);
105 | }
106 | }
107 |
108 | @end
109 |
--------------------------------------------------------------------------------
/Source/Classes/MUPublicServerListController.m:
--------------------------------------------------------------------------------
1 | // Copyright 2009-2010 The 'Mumble for iOS' Developers. All rights reserved.
2 | // Use of this source code is governed by a BSD-style
3 | // license that can be found in the LICENSE file.
4 |
5 | #import "MUPublicServerList.h"
6 | #import "MUPublicServerListController.h"
7 | #import "MUCountryServerListController.h"
8 | #import "MUTableViewHeaderLabel.h"
9 | #import "MUImage.h"
10 | #import "MUBackgroundView.h"
11 |
12 | @interface MUPublicServerListController () {
13 | MUPublicServerList *_serverList;
14 | }
15 | @end
16 |
17 | @implementation MUPublicServerListController
18 |
19 | - (id) init {
20 | if ((self = [super initWithStyle:UITableViewStyleGrouped])) {
21 | _serverList = [[MUPublicServerList alloc] init];
22 | }
23 | return self;
24 | }
25 |
26 | - (void) viewWillAppear:(BOOL)animated {
27 | [super viewWillAppear:YES];
28 |
29 | self.navigationItem.title = NSLocalizedString(@"Public Servers", nil);
30 |
31 | self.tableView.backgroundView = [MUBackgroundView backgroundView];
32 |
33 | if (@available(iOS 7, *)) {
34 | self.tableView.separatorStyle = UITableViewCellSeparatorStyleSingleLine;
35 | self.tableView.separatorInset = UIEdgeInsetsZero;
36 | } else {
37 | self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
38 | }
39 |
40 | if (![_serverList isParsed]) {
41 | UIActivityIndicatorView *activityIndicatorView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhite];
42 | UIBarButtonItem *barActivityIndicator = [[UIBarButtonItem alloc] initWithCustomView:activityIndicatorView];
43 | self.navigationItem.rightBarButtonItem = barActivityIndicator;
44 | [activityIndicatorView startAnimating];
45 | }
46 | }
47 |
48 | - (void) viewDidAppear:(BOOL)animated {
49 | [super viewDidAppear:YES];
50 |
51 | dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
52 | if ([self->_serverList isParsed]) {
53 | self.navigationItem.rightBarButtonItem = nil;
54 | return;
55 | }
56 | [self->_serverList parse];
57 | dispatch_async(dispatch_get_main_queue(), ^{
58 | self.navigationItem.rightBarButtonItem = nil;
59 | [self.tableView reloadData];
60 | });
61 | });
62 | }
63 |
64 | #pragma mark -
65 | #pragma mark UITableView data source
66 |
67 | - (NSInteger) numberOfSectionsInTableView:(UITableView *)tableView {
68 | return [_serverList numberOfContinents];
69 | }
70 |
71 | - (UIView *) tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section {
72 | return [MUTableViewHeaderLabel labelWithText:[_serverList continentNameAtIndex:section]];
73 | }
74 |
75 | - (CGFloat) tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section {
76 | return [MUTableViewHeaderLabel defaultHeaderHeight];
77 | }
78 |
79 |
80 | - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
81 | return [_serverList numberOfCountriesAtContinentIndex:section];
82 | }
83 |
84 | - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
85 | return 50.0;
86 | }
87 |
88 | - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
89 | UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"countryItem"];
90 | if (!cell) {
91 | cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:@"countryItem"];
92 | }
93 |
94 | [cell setAccessoryType:UITableViewCellAccessoryDisclosureIndicator];
95 | NSDictionary *countryInfo = [_serverList countryAtIndexPath:indexPath];
96 | cell.textLabel.text = [countryInfo objectForKey:@"name"];
97 | NSInteger numServers = [[countryInfo objectForKey:@"servers"] count];
98 | cell.detailTextLabel.text = [NSString stringWithFormat:@"%li %@", (long int)numServers, numServers > 1 ? @"servers" : @"server"];
99 | cell.selectionStyle = UITableViewCellSelectionStyleGray;
100 |
101 | return cell;
102 | }
103 |
104 | #pragma mark -
105 | #pragma mark UITableView delegate
106 |
107 | - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
108 | NSDictionary *countryInfo = [_serverList countryAtIndexPath:indexPath];
109 | NSString *countryName = [countryInfo objectForKey:@"name"];
110 | NSArray *countryServers = [countryInfo objectForKey:@"servers"];
111 |
112 | MUCountryServerListController *countryController = [[MUCountryServerListController alloc] initWithName:countryName serverList:countryServers];
113 | [[self navigationController] pushViewController:countryController animated:YES];
114 | }
115 |
116 | @end
117 |
--------------------------------------------------------------------------------
/Source/Classes/MUAudioSidetonePreferencesViewController.m:
--------------------------------------------------------------------------------
1 | // Copyright 2009-2011 The 'Mumble for iOS' Developers. All rights reserved.
2 | // Use of this source code is governed by a BSD-style
3 | // license that can be found in the LICENSE file.
4 |
5 | #import "MUAudioSidetonePreferencesViewController.h"
6 | #import "MUTableViewHeaderLabel.h"
7 | #import "MUColor.h"
8 | #import "MUImage.h"
9 | #import "MUBackgroundView.h"
10 |
11 | @implementation MUAudioSidetonePreferencesViewController
12 |
13 | - (id) init {
14 | if ((self = [super initWithStyle:UITableViewStyleGrouped])) {
15 | self.preferredContentSize = CGSizeMake(320, 480);
16 | }
17 | return self;
18 | }
19 |
20 | - (void) viewWillAppear:(BOOL)animated {
21 | [super viewWillAppear:animated];
22 |
23 | self.title = NSLocalizedString(@"Sidetone", nil);
24 |
25 | self.tableView.backgroundView = [MUBackgroundView backgroundView];
26 |
27 | if (@available(iOS 7, *)) {
28 | self.tableView.separatorStyle = UITableViewCellSeparatorStyleSingleLine;
29 | self.tableView.separatorInset = UIEdgeInsetsZero;
30 | } else {
31 | self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
32 | }
33 |
34 | self.tableView.scrollEnabled = NO;
35 | }
36 |
37 | #pragma mark - Table view data source
38 |
39 | - (NSInteger) numberOfSectionsInTableView:(UITableView *)tableView {
40 | return 1;
41 | }
42 |
43 | - (NSInteger) tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
44 | return 2;
45 | }
46 |
47 | - (UITableViewCell *) tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
48 | static NSString *CellIdentifier = @"MUAudioSidetonePreferencesCell";
49 | UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
50 | if (cell == nil) {
51 | cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier];
52 | }
53 |
54 | NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
55 |
56 | cell.selectionStyle = UITableViewCellSelectionStyleGray;
57 | cell.accessoryView = nil;
58 | cell.selectionStyle = UITableViewCellSelectionStyleNone;
59 |
60 | if ([indexPath section] == 0) {
61 | if ([indexPath row] == 0) {
62 | cell.textLabel.text = NSLocalizedString(@"Enable Sidetone", nil);
63 | UISwitch *sidetoneSwitch = [[UISwitch alloc] init];
64 | [sidetoneSwitch setOnTintColor:[UIColor blackColor]];
65 | [sidetoneSwitch addTarget:self action:@selector(sidetoneStatusChanged:) forControlEvents:UIControlEventValueChanged];
66 | [sidetoneSwitch setOn:[defaults boolForKey:@"AudioSidetone"]];
67 | cell.accessoryView = sidetoneSwitch;
68 | } else if ([indexPath row] == 1) {
69 | NSLog(@"reloadin' (enabled? %u)", [defaults boolForKey:@"AudioSidetone"]);
70 | cell.textLabel.text = NSLocalizedString(@"Playback Volume", nil);
71 | UISlider *sidetoneSlider = [[UISlider alloc] init];
72 | [sidetoneSlider addTarget:self action:@selector(sidetoneVolumeChanged:) forControlEvents:UIControlEventValueChanged];
73 | [sidetoneSlider setEnabled:[defaults boolForKey:@"AudioSidetone"]];
74 | [sidetoneSlider setMinimumValue:0.0f];
75 | [sidetoneSlider setMaximumValue:1.0f];
76 | [sidetoneSlider setValue:[defaults floatForKey:@"AudioSidetoneVolume"]];
77 | [sidetoneSlider setMinimumTrackTintColor:[UIColor blackColor]];
78 | cell.accessoryView = sidetoneSlider;
79 | }
80 | }
81 |
82 | return cell;
83 | }
84 |
85 | - (UIView *) tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section {
86 | if (section == 0) {
87 | return [MUTableViewHeaderLabel labelWithText:NSLocalizedString(@"Sidetone Feedback", nil)];
88 | }
89 | return nil;
90 | }
91 |
92 | - (CGFloat) tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section {
93 | if (section == 0) {
94 | return [MUTableViewHeaderLabel defaultHeaderHeight];
95 | }
96 | return 0.0f;
97 | }
98 |
99 | #pragma mark - Actions
100 |
101 | - (void) sidetoneStatusChanged:(UISwitch *)sidetoneSwitch {
102 | NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
103 | [defaults setBool:[sidetoneSwitch isOn] forKey:@"AudioSidetone"];
104 | [[self tableView] reloadRowsAtIndexPaths:[NSArray arrayWithObject:[NSIndexPath indexPathForRow:1 inSection:0]] withRowAnimation:UITableViewRowAnimationNone];
105 | }
106 |
107 | - (void) sidetoneVolumeChanged:(UISlider *)sidetoneSlider {
108 | NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
109 | [defaults setFloat:[sidetoneSlider value] forKey:@"AudioSidetoneVolume"];
110 | }
111 |
112 | @end
113 |
--------------------------------------------------------------------------------
/Source/Classes/MUMessageAttachmentViewController.m:
--------------------------------------------------------------------------------
1 | // Copyright 2009-2012 The 'Mumble for iOS' Developers. All rights reserved.
2 | // Use of this source code is governed by a BSD-style
3 | // license that can be found in the LICENSE file.
4 |
5 | #import "MUMessageAttachmentViewController.h"
6 | #import "MUTableViewHeaderLabel.h"
7 | #import "MUImageViewController.h"
8 | #import "MUImage.h"
9 | #import "MUBackgroundView.h"
10 |
11 | @interface MUMessageAttachmentViewController () {
12 | NSArray *_links;
13 | NSArray *_images;
14 | }
15 | @end
16 |
17 | @implementation MUMessageAttachmentViewController
18 |
19 | - (id) initWithImages:(NSArray *)images andLinks:(NSArray *)links {
20 | if ((self = [super initWithStyle:UITableViewStyleGrouped])) {
21 | _images = images;
22 | _links = links;
23 | }
24 | return self;
25 | }
26 |
27 | #pragma mark - View lifecycle
28 |
29 | - (void) viewWillAppear:(BOOL)animated {
30 | [super viewWillAppear:animated];
31 |
32 | self.navigationItem.title = NSLocalizedString(@"Attachments", nil);
33 |
34 | self.tableView.backgroundView = [MUBackgroundView backgroundView];
35 |
36 | if (@available(iOS 7, *)) {
37 | self.tableView.separatorStyle = UITableViewCellSeparatorStyleSingleLine;
38 | self.tableView.separatorInset = UIEdgeInsetsZero;
39 | } else {
40 | self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
41 | }
42 | }
43 |
44 | #pragma mark - Table view data source
45 |
46 | - (NSInteger) numberOfSectionsInTableView:(UITableView *)tableView {
47 | BOOL hasImages = [_images count] > 0;
48 | if (hasImages) {
49 | return 2;
50 | } else {
51 | return 1;
52 | }
53 | return 0;
54 | }
55 |
56 | - (NSInteger) tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
57 | BOOL hasImages = [_images count] > 0;
58 | if (hasImages && section == 0) {
59 | return 1;
60 | } else {
61 | return [_links count];
62 | }
63 | return 0;
64 | }
65 |
66 | - (UIView *) tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section {
67 | BOOL hasImages = [_images count] > 0;
68 | if (hasImages && section == 0) {
69 | return [MUTableViewHeaderLabel labelWithText:NSLocalizedString(@"Images", nil)];
70 | } else {
71 | return [MUTableViewHeaderLabel labelWithText:NSLocalizedString(@"Links", nil)];
72 | }
73 | return nil;
74 | }
75 |
76 | - (CGFloat) tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section {
77 | return [MUTableViewHeaderLabel defaultHeaderHeight];
78 | }
79 |
80 | - (UITableViewCell *) tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
81 | static NSString *CellIdentifier = @"Cell";
82 | UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
83 | if (cell == nil) {
84 | cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier];
85 | }
86 |
87 | cell.selectionStyle = UITableViewCellSelectionStyleGray;
88 |
89 | BOOL hasImages = [_images count] > 0;
90 | if (hasImages && [indexPath section] == 0) {
91 | UIImage *img = [_images objectAtIndex:0];
92 | UIImage *round = [MUImage tableViewCellImageFromImage:img];
93 | [cell.imageView setImage:round];
94 | cell.textLabel.text = NSLocalizedString(@"Images", nil);
95 | NSString *detailText = NSLocalizedString(@"1 image", nil);
96 | if ([_images count] > 1)
97 | detailText = [NSString stringWithFormat:NSLocalizedString(@"%lu images", nil), (unsigned long)[_images count]];
98 | cell.detailTextLabel.text = detailText;
99 | cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
100 | } else {
101 | cell.imageView.image = nil;
102 | NSString *urlStr = [_links objectAtIndex:[indexPath row]];
103 | NSURL *url = [NSURL URLWithString:urlStr];
104 | cell.textLabel.text = [url host];
105 | cell.detailTextLabel.text = urlStr;
106 | }
107 |
108 | return cell;
109 | }
110 |
111 | #pragma mark - Table view delegate
112 |
113 | - (void) tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
114 | BOOL hasImages = [_images count] > 0;
115 | if (hasImages && [indexPath section] == 0) {
116 | MUImageViewController *imgViewController = [[MUImageViewController alloc] initWithImages:_images];
117 | [self.navigationController pushViewController:imgViewController animated:YES];
118 | } else {
119 | [[UIApplication sharedApplication] openURL:[NSURL URLWithString:[_links objectAtIndex:[indexPath row]]] options:@{} completionHandler:nil];
120 | }
121 |
122 | [self.tableView deselectRowAtIndexPath:indexPath animated:YES];
123 | }
124 |
125 | @end
126 |
--------------------------------------------------------------------------------
/Source/Classes/MURemoteControlServer.m:
--------------------------------------------------------------------------------
1 | // Copyright 2012 The 'Mumble for iOS' Developers. All rights reserved.
2 | // Use of this source code is governed by a BSD-style
3 | // license that can be found in the LICENSE file.
4 |
5 | #include
6 | #include
7 | #include
8 | #include
9 |
10 | #import "MURemoteControlServer.h"
11 |
12 | #import
13 |
14 | @interface MURemoteControlServer () {
15 | NSMutableArray *_activeSocks;
16 | CFSocketRef _sock;
17 | }
18 | - (void) addSocket:(NSNumber *)socketNumber;
19 | - (void) removeSocket:(NSNumber *)socketNumber;
20 | @end
21 |
22 | static void *serverThread(void *udata) {
23 | int sock = (int) (long) udata;
24 | unsigned char action;
25 |
26 | int val = 1;
27 | if (setsockopt(sock, SOL_SOCKET, SO_NOSIGPIPE, &val, sizeof(val)) == -1)
28 | goto out;
29 |
30 | while (1) {
31 | ssize_t nread = read(sock, &action, 1);
32 | if (nread == 0) {
33 | goto out;
34 | }
35 | if (nread == -1) {
36 | NSLog(@"MURemoteControlServer: aborted server thread: %s", strerror(errno));
37 | goto out;
38 | }
39 |
40 | unsigned char on = action & 0x1;
41 | unsigned char code = action & ~0x1;
42 | if (code == 0) { // PTT
43 | MKAudio *audio = [MKAudio sharedAudio];
44 | [audio setForceTransmit:on > 0];
45 | }
46 | }
47 | out:
48 | @autoreleasepool {
49 | MURemoteControlServer *remoteControlServer = [MURemoteControlServer sharedRemoteControlServer];
50 | NSNumber *socketNumber = [NSNumber numberWithInt:sock];
51 | [remoteControlServer performSelector:@selector(removeSocket:) onThread:[NSThread mainThread] withObject:socketNumber waitUntilDone:NO];
52 | }
53 | return NULL;
54 | }
55 |
56 | static void acceptCallBack(CFSocketRef socket, CFSocketCallBackType type, CFDataRef address, const void *data, void *info) {
57 | MURemoteControlServer *remoteControl = (__bridge MURemoteControlServer *) info;
58 | int sock = *(int *)data;
59 | [remoteControl addSocket:[NSNumber numberWithInt:sock]];
60 | if (type == kCFSocketAcceptCallBack) {
61 | pthread_t thr;
62 | pthread_create(&thr, NULL, serverThread, (void *)((uintptr_t)sock));
63 | }
64 | }
65 |
66 | @implementation MURemoteControlServer
67 |
68 | + (MURemoteControlServer *) sharedRemoteControlServer {
69 | static dispatch_once_t token;
70 | static MURemoteControlServer *remoteControlServer;
71 | dispatch_once(&token, ^{
72 | remoteControlServer = [[MURemoteControlServer alloc] init];
73 | });
74 | return remoteControlServer;
75 | }
76 |
77 | - (id) init {
78 | if ((self = [super init])) {
79 | }
80 | return self;
81 | }
82 |
83 | - (void) addSocket:(NSNumber *)socketNumber {
84 | [_activeSocks addObject:socketNumber];
85 | }
86 |
87 | - (void) removeSocket:(NSNumber *)socketNumber {
88 | [_activeSocks removeObjectIdenticalTo:socketNumber];
89 | }
90 |
91 | - (void) closeAllSockets {
92 | for (NSNumber *numberSocket in _activeSocks) {
93 | int sock = [numberSocket intValue];
94 | close(sock);
95 | }
96 | }
97 |
98 | - (BOOL) start {
99 | _activeSocks = [[NSMutableArray alloc] init];
100 |
101 | CFSocketContext ctx = {0, (void *) CFBridgingRetain(self), NULL, NULL, NULL};
102 | _sock = CFSocketCreate(NULL, PF_INET6, SOCK_STREAM, IPPROTO_TCP,
103 | kCFSocketAcceptCallBack, (CFSocketCallBack)acceptCallBack, &ctx);
104 | if (_sock == NULL) {
105 | return NO;
106 | }
107 |
108 | int val = 1;
109 | setsockopt(CFSocketGetNative(_sock), SOL_SOCKET, SO_REUSEADDR, &val, sizeof(val));
110 |
111 | struct sockaddr_in6 addr6;
112 | memset(&addr6, 0, sizeof(addr6));
113 | addr6.sin6_len = sizeof(addr6);
114 | addr6.sin6_family = AF_INET6;
115 | addr6.sin6_port = htons(54295);
116 | addr6.sin6_flowinfo = 0;
117 | addr6.sin6_addr = in6addr_any;
118 |
119 | CFSocketError err = CFSocketSetAddress(_sock, (CFDataRef) [NSData dataWithBytes:&addr6 length:sizeof(addr6)]);
120 | if (err != kCFSocketSuccess) {
121 | CFSocketInvalidate(_sock);
122 | CFRelease(_sock);
123 | _sock = NULL;
124 | return NO;
125 | }
126 |
127 | CFRunLoopRef loop = CFRunLoopGetCurrent();
128 | CFRunLoopSourceRef src = CFSocketCreateRunLoopSource(NULL, _sock, 0);
129 | CFRunLoopAddSource(loop, src, kCFRunLoopCommonModes);
130 | CFRelease(src);
131 |
132 | return YES;
133 | }
134 |
135 | - (BOOL) stop {
136 | [self closeAllSockets];
137 | if (_sock != NULL) {
138 | CFSocketInvalidate(_sock);
139 | CFRelease(_sock);
140 | _sock = NULL;
141 | }
142 | return YES;
143 | }
144 |
145 | - (BOOL) isRunning {
146 | return _sock != NULL;
147 | }
148 |
149 | @end
150 |
--------------------------------------------------------------------------------
/Source/Classes/MUMessagesDatabase.m:
--------------------------------------------------------------------------------
1 | // Copyright 2009-2012 The 'Mumble for iOS' Developers. All rights reserved.
2 | // Use of this source code is governed by a BSD-style
3 | // license that can be found in the LICENSE file.
4 |
5 | #import "MUMessagesDatabase.h"
6 | #import "MUTextMessage.h"
7 | #import "MUDataURL.h"
8 |
9 | #import
10 |
11 | #import
12 |
13 | @interface MUMessagesDatabase () {
14 | NSCache *_msgCache;
15 | FMDatabase *_db;
16 | NSInteger _count;
17 | }
18 | @end
19 |
20 | @implementation MUMessagesDatabase
21 |
22 | - (id) init {
23 | if ((self = [super init])) {
24 | NSFileManager *manager = [NSFileManager defaultManager];
25 | NSString *directory = NSTemporaryDirectory();
26 | NSString *dbPath = [directory stringByAppendingPathComponent:@"msg.db"];
27 | [manager removeItemAtPath:dbPath error:nil];
28 | _db = [[FMDatabase alloc] initWithPath:dbPath];
29 | if (![_db open]) {
30 | NSLog(@"MUMessagesDatabse: Failed to open.");
31 | }
32 |
33 | [_db executeUpdate:@"CREATE TABLE IF NOT EXISTS `msg` "
34 | @"(`id` INTEGER PRIMARY KEY AUTOINCREMENT,"
35 | @" `rendered` BLOB,"
36 | @" `plist` BLOB)"];
37 |
38 | _msgCache = [[NSCache alloc] init];
39 | [_msgCache setCountLimit:10];
40 | }
41 | return self;
42 | }
43 |
44 | - (void) addMessage:(MKTextMessage *)msg withHeading:(NSString *)heading andSentBySelf:(BOOL)selfSent {
45 | NSError *err = nil;
46 | NSString *plainMsg = [msg plainTextString];
47 | plainMsg = [plainMsg stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
48 | NSMutableArray *imageDataArray = [[NSMutableArray alloc] initWithCapacity:[[msg embeddedImages] count]];
49 | for (NSString *dataUrl in [msg embeddedImages]) {
50 | NSData *imgData = [MUDataURL dataFromDataURL:dataUrl];
51 | if (imgData) {
52 | [imageDataArray addObject:imgData];
53 | }
54 | }
55 |
56 | NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys:
57 | heading, @"heading",
58 | plainMsg, @"msg",
59 | [NSDate date], @"date",
60 | [msg embeddedLinks], @"links",
61 | imageDataArray, @"images",
62 | [NSNumber numberWithBool:selfSent], @"selfsent",
63 | nil];
64 | NSData *plist = [NSPropertyListSerialization dataWithPropertyList:dict format:NSPropertyListBinaryFormat_v1_0 options:0 error:&err];
65 | [_db executeUpdate:@"INSERT INTO `msg` (`rendered`, `plist`) VALUES (?,?)", [NSNull null], plist ? plist : [NSNull null]];
66 | _count++;
67 | }
68 |
69 | - (void) clearMessageAtIndex:(NSInteger)row {
70 | [_db executeUpdate:@"UPDATE `msg` SET `plist`=NULL, `rendered`=NULL WHERE `id`=?", [NSNumber numberWithInteger:row+1]];
71 | [_msgCache removeObjectForKey:[NSNumber numberWithInteger:row+1]];
72 | }
73 |
74 | - (MUTextMessage *) messageAtIndex:(NSInteger)row {
75 | MUTextMessage *txtMsg = [_msgCache objectForKey:[NSNumber numberWithInteger:row+1]];
76 | if (txtMsg != nil)
77 | return txtMsg;
78 |
79 | FMResultSet *result = [_db executeQuery:@"SELECT `plist` FROM `msg` WHERE `id` = ?", [NSNumber numberWithInteger:row+1]];
80 | if ([result next]) {
81 | NSData *plistData = [result dataForColumnIndex:0];
82 | if (plistData) {
83 | NSDictionary *dict = [NSPropertyListSerialization propertyListWithData:plistData options:0 format:nil error:nil];
84 | if (dict) {
85 | NSArray *imgDataArray = [dict objectForKey:@"images"];
86 | NSMutableArray *imagesArray = [[NSMutableArray alloc] initWithCapacity:[imgDataArray count]];
87 | for (NSData *data in imgDataArray) {
88 | [imagesArray addObject:[UIImage imageWithData:data]];
89 | }
90 | txtMsg = [MUTextMessage textMessageWithHeading:[dict objectForKey:@"heading"]
91 | andMessage:[dict objectForKey:@"msg"]
92 | andEmbeddedLinks:[dict objectForKey:@"links"]
93 | andEmbeddedImages:imagesArray
94 | andTimestampDate:[dict objectForKey:@"date"]
95 | isSentBySelf:[[dict objectForKey:@"selfsent"] boolValue]];
96 | [_msgCache setObject:txtMsg forKey:[NSNumber numberWithInteger:row+1]];
97 | return txtMsg;
98 | }
99 | }
100 | }
101 | return nil;
102 | }
103 |
104 | - (NSInteger) count {
105 | return _count;
106 | }
107 |
108 | @end
109 |
--------------------------------------------------------------------------------
/Source/Classes/MUCertificateController.m:
--------------------------------------------------------------------------------
1 | // Copyright 2009-2010 The 'Mumble for iOS' Developers. All rights reserved.
2 | // Use of this source code is governed by a BSD-style
3 | // license that can be found in the LICENSE file.
4 |
5 | #import "MUCertificateController.h"
6 | #import
7 |
8 | @implementation MUCertificateController
9 |
10 | // Retrieve a certificate by its persistent reference.
11 | //
12 | // If the value stored in the keychain is of type SecIdentityRef, the
13 | // returned MKCertificate will include both the certificate and the
14 | // private key of the returned identity.
15 | //
16 | // If the value stored in the keychain is of type SecCertificateRef,
17 | // the returned MKCertificate will not include a private key.
18 | + (MKCertificate *) certificateWithPersistentRef:(NSData *)persistentRef {
19 | NSDictionary *query = [NSDictionary dictionaryWithObjectsAndKeys:
20 | persistentRef, kSecValuePersistentRef,
21 | kCFBooleanTrue, kSecReturnRef,
22 | kSecMatchLimitOne, kSecMatchLimit,
23 | nil];
24 | CFTypeRef thing = NULL;
25 | MKCertificate *cert = nil;
26 | if (SecItemCopyMatching((CFDictionaryRef)query, &thing) == noErr && thing != NULL) {
27 | CFTypeID receivedType = CFGetTypeID(thing);
28 | if (receivedType == SecIdentityGetTypeID()) {
29 | SecIdentityRef identity = (SecIdentityRef) thing;
30 | SecCertificateRef secCert = NULL;
31 | if (SecIdentityCopyCertificate(identity, &secCert) == noErr) {
32 | NSData *secData = (NSData *)SecCertificateCopyData(secCert);
33 | SecKeyRef secKey = NULL;
34 | if (SecIdentityCopyPrivateKey(identity, &secKey) == noErr) {
35 | NSData *pkeyData = nil;
36 | query = [NSDictionary dictionaryWithObjectsAndKeys:
37 | (CFTypeRef) secKey, kSecValueRef,
38 | kCFBooleanTrue, kSecReturnData,
39 | kSecMatchLimitOne, kSecMatchLimit,
40 | nil];
41 | if (SecItemCopyMatching((CFDictionaryRef)query, (CFTypeRef *)&pkeyData) == noErr) {
42 | cert = [MKCertificate certificateWithCertificate:secData privateKey:pkeyData];
43 | [pkeyData release];
44 | }
45 | CFRelease(secKey);
46 | }
47 | [secData release];
48 | }
49 | } else if (receivedType == SecCertificateGetTypeID()) {
50 | SecCertificateRef secCert = (SecCertificateRef) thing;
51 | NSData *secData = (NSData *)SecCertificateCopyData(secCert);
52 | cert = [MKCertificate certificateWithCertificate:secData privateKey:nil];
53 | [secData release];
54 | } else {
55 | return nil;
56 | }
57 | }
58 | return cert;
59 | }
60 |
61 | // Converts a hex string into a user-readable fingerprint.
62 | + (NSString *) fingerprintFromHexString:(NSString *)hexDigest {
63 | NSMutableString *fingerprint = [NSMutableString string];
64 | for (int i = 0; i < [hexDigest length]; i++) {
65 | if ((i % 2) == 0 && i > 0 && i < hexDigest.length-1) {
66 | [fingerprint appendString:@":"];
67 | }
68 | [fingerprint appendFormat:@"%C", [hexDigest characterAtIndex:i]];
69 | }
70 | return fingerprint;
71 | }
72 |
73 | // Delete the certificate referenced by the persistent reference persistentRef.
74 | // todo(mkrautz): Don't leak OSStatus.
75 | + (OSStatus) deleteCertificateWithPersistentRef:(NSData *)persistentRef {
76 | // This goes against what the documentation says for this function, but Apple has stated that
77 | // this is the intended way to delete via a persistent ref through a rdar.
78 | NSDictionary *op = [NSDictionary dictionaryWithObjectsAndKeys:
79 | persistentRef, kSecValuePersistentRef,
80 | nil];
81 | return SecItemDelete((CFDictionaryRef)op);
82 | }
83 |
84 | // Returns the certificate set as the default or 'active' certificate.
85 | + (MKCertificate *) defaultCertificate {
86 | NSData *persistentRef = [[NSUserDefaults standardUserDefaults] objectForKey:@"DefaultCertificate"];
87 | return [MUCertificateController certificateWithPersistentRef:persistentRef];
88 | }
89 |
90 | // Set the default certificate by its persistent ref.
91 | + (void) setDefaultCertificateByPersistentRef:(NSData *)persistentRef {
92 | [[NSUserDefaults standardUserDefaults] setObject:persistentRef forKey:@"DefaultCertificate"];
93 | }
94 |
95 | // Returns an array of the persistent refs of all SecIdentityRefs
96 | // stored in the application's keychain.
97 | + (NSArray *) persistentRefsForIdentities {
98 | NSDictionary *query = @{
99 | (id)kSecClass: (id)kSecClassIdentity,
100 | (id)kSecReturnPersistentRef: (id)kCFBooleanTrue,
101 | (id)kSecMatchLimit: (id)kSecMatchLimitAll
102 | };
103 | NSArray *array = nil;
104 | OSStatus err = SecItemCopyMatching((CFDictionaryRef)query, (CFTypeRef *)&array);
105 | if (err != noErr) {
106 | [array release];
107 | return nil;
108 | }
109 |
110 | return [array autorelease];
111 | }
112 |
113 | @end
114 |
--------------------------------------------------------------------------------
/Source/Classes/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
--------------------------------------------------------------------------------
/Source/Classes/MUImageViewController.m:
--------------------------------------------------------------------------------
1 | // Copyright 2009-2012 The 'Mumble for iOS' Developers. All rights reserved.
2 | // Use of this source code is governed by a BSD-style
3 | // license that can be found in the LICENSE file.
4 |
5 | #import "MUImageViewController.h"
6 | #import "MUColor.h"
7 |
8 | @interface MUImageViewController () {
9 | NSArray *_images;
10 | NSArray *_imageViews;
11 | UIScrollView *_scrollView;
12 | NSUInteger _curPage;
13 | }
14 | @end
15 |
16 | @implementation MUImageViewController
17 |
18 | - (id) initWithImages:(NSArray *)images {
19 | if ((self = [super init])) {
20 | _images = images;
21 | _curPage = 0;
22 | }
23 | return self;
24 | }
25 |
26 | - (void) viewDidLoad {
27 | [super viewDidLoad];
28 |
29 | CGRect frame = CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height-44);
30 |
31 | _scrollView = [[UIScrollView alloc] initWithFrame:frame];
32 | [_scrollView setDelegate:self];
33 | [_scrollView setPagingEnabled:YES];
34 | [_scrollView setMaximumZoomScale:1.0f];
35 | [_scrollView setMinimumZoomScale:1.0f];
36 | [_scrollView setShowsVerticalScrollIndicator:NO];
37 | [_scrollView setShowsHorizontalScrollIndicator:NO];
38 |
39 | CGRect contentFrame = CGRectMake(0, 0, frame.size.width * [_images count], frame.size.height);
40 | [_scrollView setContentSize:contentFrame.size];
41 | NSMutableArray *imageViews = [[NSMutableArray alloc] initWithCapacity:[_images count]];
42 |
43 | NSUInteger i = 0;
44 | for (i = 0; i < [_images count]; i++) {
45 | CGRect imageFrame = CGRectMake(frame.size.width*i, 0, frame.size.width, frame.size.height);
46 | UIScrollView *imgZoomer = [[UIScrollView alloc] initWithFrame:imageFrame];
47 | UIImageView *imgView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, imageFrame.size.width, imageFrame.size.height)];
48 | [imgView setImage:[_images objectAtIndex:i]];
49 | [imgView setContentMode:UIViewContentModeScaleAspectFit];
50 | [imgZoomer setDelegate:self];
51 | [imgZoomer addSubview:imgView];
52 | [imgZoomer setMaximumZoomScale:4.0f];
53 | [imgZoomer setMinimumZoomScale:1.0f];
54 | [imgZoomer setShowsVerticalScrollIndicator:NO];
55 | [imgZoomer setShowsHorizontalScrollIndicator:NO];
56 | [_scrollView addSubview:imgZoomer];
57 | [imageViews addObject:imgView];
58 | ++i;
59 | }
60 |
61 | _imageViews = imageViews;
62 |
63 | [self.view addSubview:_scrollView];
64 | }
65 |
66 | - (void) viewWillAppear:(BOOL)animated {
67 | [super viewWillAppear:animated];
68 |
69 | self.navigationItem.title = [NSString stringWithFormat:NSLocalizedString(@"%lu of %lu", nil), (unsigned long)1, (unsigned long)[_images count]];
70 |
71 | if (@available(iOS 7, *)) {
72 | _scrollView.backgroundColor = [MUColor backgroundViewiOS7Color];
73 | }
74 |
75 | UIBarButtonItem *actionButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAction target:self action:@selector(actionClicked:)];
76 | self.navigationItem.rightBarButtonItem = actionButton;
77 | }
78 |
79 | #pragma mark - UIScrollViewDelegate
80 |
81 | - (void) scrollViewDidScroll:(UIScrollView *)scrollView {
82 | if (scrollView == _scrollView) {
83 | CGPoint pt = [_scrollView contentOffset];
84 | NSInteger pg = (NSInteger)(pt.x / self.view.frame.size.width);
85 | if (pg != _curPage) {
86 | _curPage = pg;
87 | self.navigationItem.title = [NSString stringWithFormat:NSLocalizedString(@"%lu of %lu", nil), (unsigned long)1+_curPage, (unsigned long)[_images count]];
88 | }
89 | }
90 | }
91 |
92 | - (UIView *) viewForZoomingInScrollView:(UIScrollView *)scrollView {
93 | if (scrollView != _scrollView) {
94 | return [_imageViews objectAtIndex:_curPage];
95 | }
96 | return nil;
97 | }
98 |
99 | #pragma mark - Actions
100 |
101 | - (void) image:(UIImage *)img didFinishSavingWithError:(NSError *)err contextInfo:(void *)userInfo {
102 | if (err != nil) {
103 | UIAlertController* alertCtrl = [UIAlertController alertControllerWithTitle:NSLocalizedString(@"Unable to save image", nil)
104 | message:[err description]
105 | preferredStyle:UIAlertControllerStyleAlert];
106 | [alertCtrl addAction: [UIAlertAction actionWithTitle:NSLocalizedString(@"OK", nil)
107 | style:UIAlertActionStyleCancel
108 | handler:nil]];
109 |
110 | [self presentViewController:alertCtrl animated:YES completion:nil];
111 | }
112 | }
113 |
114 | - (void) actionClicked:(id)sender {
115 | UIAlertController* alertCtrl = [UIAlertController alertControllerWithTitle:NSLocalizedString(@"Export Image", nil)
116 | message:nil
117 | preferredStyle:UIAlertControllerStyleActionSheet];
118 | [alertCtrl addAction: [UIAlertAction actionWithTitle:NSLocalizedString(@"Cancel", nil)
119 | style:UIAlertActionStyleCancel
120 | handler:nil]];
121 | [alertCtrl addAction: [UIAlertAction actionWithTitle:NSLocalizedString(@"Export to Photos", nil)
122 | style:UIAlertActionStyleDefault
123 | handler:^(UIAlertAction * _Nonnull action) {
124 | UIImageWriteToSavedPhotosAlbum([self->_images objectAtIndex:self->_curPage], self, @selector(image:didFinishSavingWithError:contextInfo:), NULL);
125 | }]];
126 |
127 | [self presentViewController:alertCtrl animated:YES completion:nil];
128 | }
129 |
130 | @end
131 |
--------------------------------------------------------------------------------
/Source/Classes/MUServerCell.m:
--------------------------------------------------------------------------------
1 | // Copyright 2009-2011 The 'Mumble for iOS' Developers. All rights reserved.
2 | // Use of this source code is governed by a BSD-style
3 | // license that can be found in the LICENSE file.
4 |
5 | #import "MUServerCell.h"
6 | #import "MUColor.h"
7 | #import "MUFavouriteServer.h"
8 |
9 | @interface MUServerCell () {
10 | NSString *_displayname;
11 | NSString *_hostname;
12 | NSString *_port;
13 | NSString *_username;
14 | MKServerPinger *_pinger;
15 | }
16 | - (UIImage *) drawPingImageWithPingValue:(NSUInteger)pingMs andUserCount:(NSUInteger)userCount isFull:(BOOL)isFull;
17 | @end
18 |
19 | @implementation MUServerCell
20 |
21 | + (NSString *) reuseIdentifier {
22 | return @"ServerCell";
23 | }
24 |
25 | - (id) init {
26 | return [super initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:[MUServerCell reuseIdentifier]];
27 | }
28 |
29 | - (void) populateFromDisplayName:(NSString *)displayName hostName:(NSString *)hostName port:(NSString *)port {
30 | _displayname = [displayName copy];
31 |
32 | _port = [port copy];
33 |
34 | _pinger = nil;
35 |
36 | if ([hostName length] > 0) {
37 | _hostname = [hostName copy];
38 | _pinger = [[MKServerPinger alloc] initWithHostname:_hostname port:_port];
39 | [_pinger setDelegate:self];
40 | } else {
41 | _hostname = NSLocalizedString(@"(No Server)", nil);
42 | }
43 |
44 | self.textLabel.text = _displayname;
45 | self.detailTextLabel.text = [NSString stringWithFormat:@"%@:%@", _hostname, _port];
46 | self.imageView.image = [self drawPingImageWithPingValue:999 andUserCount:0 isFull:NO];
47 | }
48 |
49 | - (void) populateFromFavouriteServer:(MUFavouriteServer *)favServ {
50 | _displayname = [[favServ displayName] copy];
51 |
52 | _hostname = [[favServ hostName] copy];
53 |
54 | _port = [NSString stringWithFormat:@"%lu", (unsigned long)[favServ port]];
55 |
56 | if ([[favServ userName] length] > 0) {
57 | _username = [[favServ userName] copy];
58 | } else {
59 | _username = [[[NSUserDefaults standardUserDefaults] objectForKey:@"DefaultUserName"] copy];
60 | }
61 |
62 | _pinger = nil;
63 | if ([_hostname length] > 0) {
64 | _pinger = [[MKServerPinger alloc] initWithHostname:_hostname port:_port];
65 | [_pinger setDelegate:self];
66 | } else {
67 | _hostname = NSLocalizedString(@"(No Server)", nil);
68 | }
69 |
70 | self.textLabel.text = _displayname;
71 | self.detailTextLabel.text = [NSString stringWithFormat:NSLocalizedString(@"%@ on %@:%@", @"username on hostname:port"),
72 | _username, _hostname, _port];
73 | self.imageView.image = [self drawPingImageWithPingValue:999 andUserCount:0 isFull:NO];
74 | }
75 |
76 | - (void) setSelected:(BOOL)selected animated:(BOOL)animated {
77 | [super setSelected:selected animated:animated];
78 | }
79 |
80 | - (UIImage *) drawPingImageWithPingValue:(NSUInteger)pingMs andUserCount:(NSUInteger)userCount isFull:(BOOL)isFull {
81 | UIImage *img = nil;
82 |
83 | UIColor *pingColor = [MUColor badPingColor];
84 | if (pingMs <= 125)
85 | pingColor = [MUColor goodPingColor];
86 | else if (pingMs > 125 && pingMs <= 250)
87 | pingColor = [MUColor mediumPingColor];
88 | else if (pingMs > 250)
89 | pingColor = [MUColor badPingColor];
90 | NSString *pingStr = [NSString stringWithFormat:@"%lu\nms", (unsigned long)pingMs];
91 | if (pingMs >= 999)
92 | pingStr = @"∞\nms";
93 |
94 | UIGraphicsBeginImageContextWithOptions(CGSizeMake(66.0f, 32.0f), NO, [[UIScreen mainScreen] scale]);
95 | CGContextRef ctx = UIGraphicsGetCurrentContext();
96 | CGContextSetFillColorWithColor(ctx, pingColor.CGColor);
97 | CGContextFillRect(ctx, CGRectMake(0, 0, 32.0, 32.0));
98 |
99 | CGContextSetTextDrawingMode(ctx, kCGTextFill);
100 | CGContextSetFillColorWithColor(ctx, [UIColor whiteColor].CGColor);
101 | NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init];
102 | paragraphStyle.lineBreakMode = NSLineBreakByTruncatingTail;
103 | paragraphStyle.alignment = NSTextAlignmentCenter;
104 | [pingStr drawInRect:CGRectMake(0.0, 0.0, 32.0, 32.0) withAttributes:@{
105 | NSFontAttributeName : [UIFont boldSystemFontOfSize: 12],
106 | NSParagraphStyleAttributeName : paragraphStyle,
107 | NSForegroundColorAttributeName : [UIColor whiteColor]
108 | }];
109 |
110 | if (!isFull) {
111 | // Non-full servers get the mild iOS blue color
112 | CGContextSetFillColorWithColor(ctx, [MUColor userCountColor].CGColor);
113 | } else {
114 | // Mark full servers with the same red as we use for
115 | // 'bad' pings...
116 | CGContextSetFillColorWithColor(ctx, [MUColor badPingColor].CGColor);
117 | }
118 | CGContextFillRect(ctx, CGRectMake(34.0, 0, 32.0, 32.0));
119 |
120 | CGContextSetTextDrawingMode(ctx, kCGTextFill);
121 | CGContextSetFillColorWithColor(ctx, [UIColor whiteColor].CGColor);
122 | NSString *usersStr = [NSString stringWithFormat:NSLocalizedString(@"%lu\nppl", @"user count"), (unsigned long)userCount];
123 | paragraphStyle = [[NSMutableParagraphStyle alloc] init];
124 | paragraphStyle.lineBreakMode = NSLineBreakByTruncatingTail;
125 | paragraphStyle.alignment = NSTextAlignmentCenter;
126 | [usersStr drawInRect:CGRectMake(34.0, 0.0, 32.0, 32.0) withAttributes:@{
127 | NSFontAttributeName : [UIFont boldSystemFontOfSize: 12],
128 | NSParagraphStyleAttributeName : paragraphStyle,
129 | NSForegroundColorAttributeName : [UIColor whiteColor]
130 | }];
131 |
132 | img = UIGraphicsGetImageFromCurrentImageContext();
133 | UIGraphicsEndImageContext();
134 |
135 | return img;
136 | }
137 |
138 | - (void) serverPingerResult:(MKServerPingerResult *)result {
139 | NSUInteger pingValue = (NSUInteger)(result->ping * 1000.0f);
140 | NSUInteger userCount = (NSUInteger)(result->cur_users);
141 | BOOL isFull = result->cur_users == result->max_users;
142 | self.imageView.image = [self drawPingImageWithPingValue:pingValue andUserCount:userCount isFull:isFull];
143 | }
144 |
145 | @end
146 |
--------------------------------------------------------------------------------
/Source/Classes/MUAudioQualityPreferencesViewController.m:
--------------------------------------------------------------------------------
1 | // Copyright 2009-2011 The 'Mumble for iOS' Developers. All rights reserved.
2 | // Use of this source code is governed by a BSD-style
3 | // license that can be found in the LICENSE file.
4 |
5 | #import "MUAudioQualityPreferencesViewController.h"
6 | #import "MUTableViewHeaderLabel.h"
7 | #import "MUColor.h"
8 | #import "MUImage.h"
9 | #import "MUBackgroundView.h"
10 |
11 | @implementation MUAudioQualityPreferencesViewController
12 |
13 | - (id) init {
14 | if ((self = [super initWithStyle:UITableViewStyleGrouped])) {
15 | self.preferredContentSize = CGSizeMake(320, 480);
16 | }
17 | return self;
18 | }
19 |
20 | - (void) viewWillAppear:(BOOL)animated {
21 | [super viewWillAppear:animated];
22 |
23 | self.title = NSLocalizedString(@"Audio Quality", nil);
24 |
25 | self.tableView.backgroundView = [MUBackgroundView backgroundView];
26 |
27 | if (@available(iOS 7, *)) {
28 | self.tableView.separatorStyle = UITableViewCellSeparatorStyleSingleLine;
29 | self.tableView.separatorInset = UIEdgeInsetsZero;
30 | } else {
31 | self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
32 | }
33 |
34 | self.tableView.scrollEnabled = NO;
35 | }
36 |
37 | #pragma mark - Table view data source
38 |
39 | - (NSInteger) numberOfSectionsInTableView:(UITableView *)tableView {
40 | return 1;
41 | }
42 |
43 | - (NSInteger) tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
44 | return 3;
45 | }
46 |
47 | - (UITableViewCell *) tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
48 | static NSString *CellIdentifier = @"MUAudioQualityPreferencesCell";
49 | UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
50 | if (cell == nil) {
51 | cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier];
52 | }
53 |
54 | NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
55 |
56 | cell.selectionStyle = UITableViewCellSelectionStyleGray;
57 | cell.accessoryView = nil;
58 |
59 | if ([indexPath section] == 0) {
60 | if ([indexPath row] == 0) {
61 | cell.textLabel.text = NSLocalizedString(@"Low", nil);
62 | cell.detailTextLabel.text = NSLocalizedString(@"16 kbit/s, 60 ms audio per packet", nil);
63 | if ([[defaults stringForKey:@"AudioQualityKind"] isEqualToString:@"low"]) {
64 | cell.accessoryView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"GrayCheckmark"]];
65 | cell.textLabel.textColor = [MUColor selectedTextColor];
66 | }
67 | } else if ([indexPath row] == 1) {
68 | cell.textLabel.text = NSLocalizedString(@"Balanced", nil);
69 | cell.detailTextLabel.text = NSLocalizedString(@"40 kbit/s, 20 ms audio per packet", nil);
70 | if ([[defaults stringForKey:@"AudioQualityKind"] isEqualToString:@"balanced"]) {
71 | cell.accessoryView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"GrayCheckmark"]];
72 | cell.textLabel.textColor = [MUColor selectedTextColor];
73 | }
74 | } else if ([indexPath row] == 2) {
75 | cell.textLabel.text = NSLocalizedString(@"High", nil);
76 | cell.detailTextLabel.text = NSLocalizedString(@"72 kbit/s, 10 ms audio per packet", nil);
77 | if ([[defaults stringForKey:@"AudioQualityKind"] isEqualToString:@"high"]) {
78 | cell.accessoryView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"GrayCheckmark"]];
79 | cell.textLabel.textColor = [MUColor selectedTextColor];
80 | }
81 | } else if ([indexPath row] == 3) {
82 | cell.textLabel.text = NSLocalizedString(@"Custom", nil);
83 | cell.detailTextLabel.text = nil;
84 | if ([[defaults stringForKey:@"AudioQualityKind"] isEqualToString:@"custom"]) {
85 | cell.accessoryView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"GrayCheckmark"]];
86 | cell.textLabel.textColor = [MUColor selectedTextColor];
87 | }
88 | }
89 | }
90 |
91 | return cell;
92 | }
93 |
94 | - (UIView *) tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section {
95 | if (section == 0) { // Input
96 | return [MUTableViewHeaderLabel labelWithText:NSLocalizedString(@"Quality Presets", nil)];
97 | } else if (section == 1) {
98 | return [MUTableViewHeaderLabel labelWithText:NSLocalizedString(@"Custom Quality", nil)];
99 | } else {
100 | return nil;
101 | }
102 | }
103 |
104 | - (CGFloat) tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section {
105 | if (section == 0) {
106 | return [MUTableViewHeaderLabel defaultHeaderHeight];
107 | } else if (section == 1) {
108 | return [MUTableViewHeaderLabel defaultHeaderHeight];
109 | }
110 | return 0.0f;
111 | }
112 |
113 | #pragma mark - Table view delegate
114 |
115 | - (void) tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
116 | [self.tableView deselectRowAtIndexPath:indexPath animated:YES];
117 | UITableViewCell *cell = nil;
118 | int nsects = 3;
119 | for (int i = 0; i <= nsects; i++) {
120 | cell = [self.tableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:i inSection:0]];
121 | cell.accessoryView = nil;
122 | cell.textLabel.textColor = [UIColor blackColor];
123 | }
124 | cell = [self.tableView cellForRowAtIndexPath:indexPath];
125 | cell.accessoryView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"GrayCheckmark"]];
126 | cell.textLabel.textColor = [MUColor selectedTextColor];
127 | NSString *val = nil;
128 | switch ([indexPath row]) {
129 | case 0: val = @"low"; break;
130 | case 1: val = @"balanced"; break;
131 | case 2: val = @"high"; break;
132 | case 3: val = @"custom"; break;
133 | }
134 | if (val != nil)
135 | [[NSUserDefaults standardUserDefaults] setObject:val forKey:@"AudioQualityKind"];
136 | }
137 |
138 | @end
139 |
--------------------------------------------------------------------------------
/Source/Classes/MUCertificateCreationProgressView.xib:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
48 |
49 |
50 |
51 |
52 |
61 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
--------------------------------------------------------------------------------
/Source/Classes/MUPublicServerList.m:
--------------------------------------------------------------------------------
1 | // Copyright 2009-2010 The 'Mumble for iOS' Developers. All rights reserved.
2 | // Use of this source code is governed by a BSD-style
3 | // license that can be found in the LICENSE file.
4 |
5 | #import "MUPublicServerList.h"
6 | #import
7 |
8 | @interface MUPublicServerList () {
9 | NSData *_serverListXML;
10 | NSMutableDictionary *_continentCountries;
11 | NSMutableDictionary *_countryServers;
12 | NSDictionary *_continentNames;
13 | NSDictionary *_countryNames;
14 | NSMutableArray *_modelContinents;
15 | NSMutableArray *_modelCountries;
16 | BOOL _parsed;
17 | }
18 | + (NSString *) filePath;
19 | @end
20 |
21 | @interface MUPublicServerListFetcher () {}
22 | @end
23 |
24 | @implementation MUPublicServerListFetcher
25 |
26 | - (id) init {
27 | if ((self = [super init])) {
28 | // ...
29 | }
30 | return self;
31 | }
32 |
33 | - (void) attemptUpdate {
34 | NSURLRequest *req = [NSURLRequest requestWithURL:[MKServices regionalServerListURL]];
35 | [[NSURLSession sharedSession] dataTaskWithRequest:req completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
36 | if (error || data == nil) {
37 | return;
38 | }
39 |
40 | [data writeToFile:[MUPublicServerList filePath] atomically:YES];
41 | }];
42 | }
43 |
44 | @end
45 |
46 |
47 | @implementation MUPublicServerList
48 |
49 | + (NSString *) filePath {
50 | NSArray *documentDirectories = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory,
51 | NSUserDomainMask,
52 | YES);
53 | NSString *directory = [documentDirectories objectAtIndex:0];
54 | [[NSFileManager defaultManager] createDirectoryAtPath:directory withIntermediateDirectories:YES attributes:nil error:nil];
55 | return [directory stringByAppendingPathComponent:@"publist.xml"];
56 | }
57 |
58 | - (id) init {
59 | if ((self = [super init])) {
60 | if ([[NSFileManager defaultManager] fileExistsAtPath:[MUPublicServerList filePath]]) {
61 | _serverListXML = [[NSData alloc] initWithContentsOfFile:[MUPublicServerList filePath]];
62 | } else {
63 | _serverListXML = [[NSData alloc] initWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"publist" ofType:@"xml"]];
64 | }
65 |
66 | _continentNames = [[NSDictionary alloc] initWithContentsOfFile: [NSString stringWithFormat:@"%@/Continents.plist", [[NSBundle mainBundle] resourcePath]]];
67 | _countryNames = [[NSDictionary alloc] initWithContentsOfFile: [NSString stringWithFormat:@"%@/Countries.plist", [[NSBundle mainBundle] resourcePath]]];
68 | }
69 | return self;
70 | }
71 |
72 | - (void) parse {
73 | // Job's done.
74 | if (_parsed)
75 | return;
76 |
77 | _continentCountries = [[NSMutableDictionary alloc] initWithCapacity:[_continentNames count]];
78 | _countryServers = [[NSMutableDictionary alloc] init];
79 |
80 | // Parse XML server list
81 | NSXMLParser *parser = [[NSXMLParser alloc] initWithData:_serverListXML];
82 | [parser setDelegate:(id)self];
83 | [parser parse];
84 |
85 | // Transform from NSDictionary representation to a NSArray-model
86 | NSArray *continentCodes = [[_continentNames allKeys] sortedArrayUsingSelector:@selector(compare:)];
87 | _modelContinents = [[NSMutableArray alloc] initWithCapacity:[continentCodes count]];
88 | _modelCountries = [[NSMutableArray alloc] init];
89 |
90 | for (NSString *key in continentCodes) {
91 | [_modelContinents addObject:[_continentNames objectForKey:key]];
92 |
93 | NSSet *countryCodeSet = [_continentCountries objectForKey:key];
94 | NSArray *countryCodes = [[countryCodeSet allObjects] sortedArrayUsingSelector:@selector(compare:)];
95 |
96 | NSMutableArray *countries = [NSMutableArray arrayWithCapacity:[countryCodes count]];
97 |
98 | for (NSString *countryKey in countryCodes) {
99 | NSString *countryName = [_countryNames objectForKey:countryKey];
100 | NSArray *countryServerList = [_countryServers objectForKey:countryKey];
101 | NSDictionary *country = [NSDictionary dictionaryWithObjectsAndKeys:
102 | countryName, @"name",
103 | countryServerList, @"servers", nil];
104 | [countries addObject:country];
105 | }
106 | [_modelCountries addObject:countries];
107 | }
108 |
109 | _continentCountries = nil;
110 | _countryServers = nil;
111 | _parsed = YES;
112 | }
113 |
114 | #pragma mark -
115 | #pragma mark NSXMLParserDelegate methods
116 |
117 | - (void) parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qualifiedName attributes:(NSDictionary *)attributeDict {
118 | if ([elementName isEqualToString:@"server"]) {
119 | NSString *countryCode = [attributeDict objectForKey:@"country_code"];
120 | if (countryCode) {
121 | // Get server array for this particular country
122 | NSMutableArray *array = [_countryServers objectForKey:countryCode];
123 | if (array == nil) {
124 | // No array available. Create a new one.
125 | array = [NSMutableArray arrayWithCapacity:50];
126 | [_countryServers setObject:array forKey:countryCode];
127 | }
128 | // Add attribute dict to server array.
129 | [array addObject:attributeDict];
130 |
131 | // Extract the continent code of the country
132 | NSString *continentCode = [attributeDict objectForKey:@"continent_code"];
133 | // Get our country set from our continent -> countries mapping
134 | NSMutableSet *countries = [_continentCountries objectForKey:continentCode];
135 | if (countries == nil) {
136 | // No set for continent? Create a new one.
137 | countries = [NSMutableSet setWithCapacity:100];
138 | [_continentCountries setObject:countries forKey:continentCode];
139 | }
140 | [countries addObject:countryCode];
141 | }
142 | }
143 | }
144 |
145 | - (void)parserDidEndDocument:(NSXMLParser *)parser {
146 | }
147 |
148 | #pragma mark -
149 | #pragma mark Model access
150 |
151 | // Returns the number of continents in the public server list
152 | - (NSInteger) numberOfContinents {
153 | return [_continentNames count];
154 | }
155 |
156 | // Get continent at index 'idx'.
157 | - (NSString *) continentNameAtIndex:(NSInteger)index {
158 | return [_modelContinents objectAtIndex:index];
159 | }
160 |
161 | // Get the number of countries in the continent at index 'idx'.
162 | - (NSInteger) numberOfCountriesAtContinentIndex:(NSInteger)index {
163 | return [[_modelCountries objectAtIndex:index] count];
164 | }
165 |
166 | // Get a dictionary representing a country.
167 | - (NSDictionary *) countryAtIndexPath:(NSIndexPath *)indexPath {
168 | return [[_modelCountries objectAtIndex:[indexPath indexAtPosition:0]] objectAtIndex:[indexPath indexAtPosition:1]];
169 | }
170 |
171 | // Return whether or not the server list has already been parsed
172 | - (BOOL) isParsed {
173 | return _parsed;
174 | }
175 |
176 | @end
177 |
--------------------------------------------------------------------------------