Browse Source

Merge branch 'website-attachment'

master
Igor Zhukov 10 years ago
parent
commit
423380716a
  1. 329
      app/js/controllers.js
  2. 11
      app/js/directives.js
  3. 10
      app/js/lib/bin_utils.js
  4. 4
      app/js/lib/config.js
  5. 21
      app/js/lib/ng_utils.js
  6. 74
      app/js/lib/schema.tl.txt
  7. 31
      app/js/lib/tl_utils.js
  8. 47
      app/js/locales/en-us.json
  9. 259
      app/js/services.js
  10. 33
      app/less/app.less
  11. 6
      app/partials/desktop/confirm_modal.html
  12. 4
      app/partials/desktop/error_modal.html
  13. 27
      app/partials/desktop/login.html
  14. 8
      app/partials/desktop/message.html
  15. 21
      app/partials/desktop/message_attach_webpage.html
  16. 32
      app/partials/desktop/password_recovery_modal.html
  17. 54
      app/partials/desktop/password_update_modal.html
  18. 14
      app/partials/desktop/settings_modal.html
  19. 294
      app/vendor/cryptoJS/crypto.js

329
app/js/controllers.js

@ -30,7 +30,7 @@ angular.module('myApp.controllers', ['myApp.i18n'])
LayoutSwitchService.start(); LayoutSwitchService.start();
}) })
.controller('AppLoginController', function ($scope, $rootScope, $location, $timeout, $modal, $modalStack, MtpApiManager, ErrorService, NotificationsManager, ChangelogNotifyService, IdleManager, LayoutSwitchService, TelegramMeWebService, _) { .controller('AppLoginController', function ($scope, $rootScope, $location, $timeout, $modal, $modalStack, MtpApiManager, ErrorService, NotificationsManager, PasswordManager, ChangelogNotifyService, IdleManager, LayoutSwitchService, TelegramMeWebService, _) {
$modalStack.dismissAll(); $modalStack.dismissAll();
IdleManager.start(); IdleManager.start();
@ -156,6 +156,7 @@ angular.module('myApp.controllers', ['myApp.i18n'])
var callTimeout; var callTimeout;
var updatePasswordTimeout = false;
function saveAuth (result) { function saveAuth (result) {
MtpApiManager.setUserAuth(options.dcID, { MtpApiManager.setUserAuth(options.dcID, {
@ -204,7 +205,8 @@ angular.module('myApp.controllers', ['myApp.i18n'])
phone_number: $scope.credentials.phone_full, phone_number: $scope.credentials.phone_full,
// sms_type: 5, // sms_type: 5,
api_id: Config.App.id, api_id: Config.App.id,
api_hash: Config.App.hash api_hash: Config.App.hash,
lang_code: navigator.language || 'en'
}, options).then(function (sentCode) { }, options).then(function (sentCode) {
$scope.progress.enabled = false; $scope.progress.enabled = false;
@ -290,6 +292,16 @@ angular.module('myApp.controllers', ['myApp.i18n'])
} else if (error.code == 400 && error.type == 'PHONE_NUMBER_OCCUPIED') { } else if (error.code == 400 && error.type == 'PHONE_NUMBER_OCCUPIED') {
error.handled = true; error.handled = true;
return $scope.logIn(false); return $scope.logIn(false);
} else if (error.code == 401 && error.type == 'SESSION_PASSWORD_NEEDED') {
$scope.progress.enabled = true;
updatePasswordState().then(function () {
$scope.progress.enabled = false;
$scope.credentials.phone_code_valid = true;
$scope.credentials.password_needed = true;
$scope.about = {};
});
error.handled = true;
return;
} }
@ -312,6 +324,82 @@ angular.module('myApp.controllers', ['myApp.i18n'])
}; };
$scope.checkPassword = function () {
return PasswordManager.check($scope.password, $scope.credentials.password, options).then(saveAuth, function (error) {
switch (error.type) {
case 'PASSWORD_HASH_INVALID':
$scope.error = {field: 'password'};
error.handled = true;
break;
}
});
};
$scope.forgotPassword = function (event) {
PasswordManager.requestRecovery($scope.password, options).then(function (emailRecovery) {
var scope = $rootScope.$new();
scope.recovery = emailRecovery;
scope.options = options;
var modal = $modal.open({
scope: scope,
templateUrl: templateUrl('password_recovery_modal'),
controller: 'PasswordRecoveryModalController',
windowClass: 'md_simple_modal_window mobile_modal'
});
modal.result.then(function (result) {
if (result && result.user) {
saveAuth(result);
} else {
$scope.canReset = true;
}
});
}, function (error) {
switch (error.type) {
case 'PASSWORD_EMPTY':
$scope.logIn();
break;
case 'PASSWORD_RECOVERY_NA':
$timeout(function () {
$scope.canReset = true;
}, 1000);
break;
}
})
return cancelEvent(event);
};
$scope.resetAccount = function () {
ErrorService.confirm({
type: 'RESET_ACCOUNT'
}).then(function () {
$scope.progress.enabled = true;
MtpApiManager.invokeApi('account.deleteAccount', {
reason: 'Forgot password'
}, options).then(function () {
delete $scope.progress.enabled;
delete $scope.credentials.password_needed;
$scope.credentials.phone_unoccupied = true;
}, function () {
delete $scope.progress.enabled;
})
});
};
function updatePasswordState () {
// $timeout.cancel(updatePasswordTimeout);
// updatePasswordTimeout = false;
return PasswordManager.getState(options).then(function (result) {
return $scope.password = result;
// if (result._ == 'account.noPassword' && result.email_unconfirmed_pattern) {
// updatePasswordTimeout = $timeout(updatePasswordState, 5000);
// }
});
}
ChangelogNotifyService.checkUpdate(); ChangelogNotifyService.checkUpdate();
LayoutSwitchService.start(); LayoutSwitchService.start();
}) })
@ -364,6 +452,8 @@ angular.module('myApp.controllers', ['myApp.i18n'])
}); });
}; };
// setTimeout($scope.openSettings, 1000);
$scope.openFaq = function () { $scope.openFaq = function () {
var url = 'https://telegram.org/faq'; var url = 'https://telegram.org/faq';
switch (Config.I18n.locale) { switch (Config.I18n.locale) {
@ -1524,7 +1614,7 @@ angular.module('myApp.controllers', ['myApp.i18n'])
$scope.$on('user_update', angular.noop); $scope.$on('user_update', angular.noop);
}) })
.controller('AppImSendController', function ($scope, $timeout, MtpApiManager, Storage, AppChatsManager, AppUsersManager, AppPeersManager, AppDocsManager, AppMessagesManager, ApiUpdatesManager, MtpApiFileManager) { .controller('AppImSendController', function ($scope, $timeout, MtpApiManager, Storage, AppChatsManager, AppUsersManager, AppPeersManager, AppDocsManager, AppMessagesManager, MtpApiFileManager) {
$scope.$watch('curDialog.peer', resetDraft); $scope.$watch('curDialog.peer', resetDraft);
$scope.$on('user_update', angular.noop); $scope.$on('user_update', angular.noop);
@ -1695,7 +1785,10 @@ angular.module('myApp.controllers', ['myApp.i18n'])
access_hash: doc.access_hash access_hash: doc.access_hash
} }
} }
AppMessagesManager.sendOther($scope.curDialog.peerID, inputMedia); var options = {
replyToMsgID: $scope.draftMessage.replyToMessage && $scope.draftMessage.replyToMessage.id
};
AppMessagesManager.sendOther($scope.curDialog.peerID, inputMedia, options);
$scope.$broadcast('ui_message_send'); $scope.$broadcast('ui_message_send');
} }
delete $scope.draftMessage.sticker; delete $scope.draftMessage.sticker;
@ -2162,8 +2255,8 @@ angular.module('myApp.controllers', ['myApp.i18n'])
MtpApiManager.invokeApi('messages.editChatPhoto', { MtpApiManager.invokeApi('messages.editChatPhoto', {
chat_id: $scope.chatID, chat_id: $scope.chatID,
photo: {_: 'inputChatPhotoEmpty'} photo: {_: 'inputChatPhotoEmpty'}
}).then(function (updateResult) { }).then(function (updates) {
AppMessagesManager.onStatedMessage(updateResult); ApiUpdatesManager.processUpdateMessage(updates);
$modalInstance.dismiss(); $modalInstance.dismiss();
$rootScope.$broadcast('history_focus', {peerString: AppChatsManager.getChatString($scope.chatID)}); $rootScope.$broadcast('history_focus', {peerString: AppChatsManager.getChatString($scope.chatID)});
})['finally'](function () { })['finally'](function () {
@ -2398,8 +2491,8 @@ angular.module('myApp.controllers', ['myApp.i18n'])
}); });
function onStatedMessage (statedMessage) { function onChatUpdated (updates) {
AppMessagesManager.onStatedMessage(statedMessage); ApiUpdatesManager.processUpdateMessage(updates);
$rootScope.$broadcast('history_focus', {peerString: $scope.chatFull.peerString}); $rootScope.$broadcast('history_focus', {peerString: $scope.chatFull.peerString});
} }
@ -2408,14 +2501,14 @@ angular.module('myApp.controllers', ['myApp.i18n'])
MtpApiManager.invokeApi('messages.deleteChatUser', { MtpApiManager.invokeApi('messages.deleteChatUser', {
chat_id: $scope.chatID, chat_id: $scope.chatID,
user_id: {_: 'inputUserSelf'} user_id: {_: 'inputUserSelf'}
}).then(onStatedMessage); }).then(onChatUpdated);
}; };
$scope.returnToGroup = function () { $scope.returnToGroup = function () {
MtpApiManager.invokeApi('messages.addChatUser', { MtpApiManager.invokeApi('messages.addChatUser', {
chat_id: $scope.chatID, chat_id: $scope.chatID,
user_id: {_: 'inputUserSelf'} user_id: {_: 'inputUserSelf'}
}).then(onStatedMessage); }).then(onChatUpdated);
}; };
@ -2431,19 +2524,8 @@ angular.module('myApp.controllers', ['myApp.i18n'])
chat_id: $scope.chatID, chat_id: $scope.chatID,
user_id: AppUsersManager.getUserInput(userID), user_id: AppUsersManager.getUserInput(userID),
fwd_limit: 100 fwd_limit: 100
}).then(function (addResult) { }).then(function (updates) {
ApiUpdatesManager.processUpdateMessage({ ApiUpdatesManager.processUpdateMessage(updates);
_: 'updates',
users: addResult.users,
chats: addResult.chats,
seq: 0,
updates: [{
_: 'updateNewMessage',
message: addResult.message,
pts: addResult.pts,
pts_count: addResult.pts_count
}]
});
}); });
}); });
@ -2457,7 +2539,7 @@ angular.module('myApp.controllers', ['myApp.i18n'])
MtpApiManager.invokeApi('messages.deleteChatUser', { MtpApiManager.invokeApi('messages.deleteChatUser', {
chat_id: $scope.chatID, chat_id: $scope.chatID,
user_id: {_: 'inputUserForeign', user_id: userID, access_hash: user.access_hash || '0'} user_id: {_: 'inputUserForeign', user_id: userID, access_hash: user.access_hash || '0'}
}).then(onStatedMessage); }).then(onChatUpdated);
}; };
@ -2488,9 +2570,7 @@ angular.module('myApp.controllers', ['myApp.i18n'])
file: inputFile, file: inputFile,
crop: {_: 'inputPhotoCropAuto'} crop: {_: 'inputPhotoCropAuto'}
} }
}).then(function (updateResult) { }).then(onChatUpdated);
onStatedMessage(updateResult);
});
})['finally'](function () { })['finally'](function () {
$scope.photo.updating = false; $scope.photo.updating = false;
}); });
@ -2501,9 +2581,7 @@ angular.module('myApp.controllers', ['myApp.i18n'])
MtpApiManager.invokeApi('messages.editChatPhoto', { MtpApiManager.invokeApi('messages.editChatPhoto', {
chat_id: $scope.chatID, chat_id: $scope.chatID,
photo: {_: 'inputChatPhotoEmpty'} photo: {_: 'inputChatPhotoEmpty'}
}).then(function (updateResult) { }).then(onChatUpdated)['finally'](function () {
onStatedMessage(updateResult);
})['finally'](function () {
$scope.photo.updating = false; $scope.photo.updating = false;
}); });
}; };
@ -2522,7 +2600,7 @@ angular.module('myApp.controllers', ['myApp.i18n'])
}) })
.controller('SettingsModalController', function ($rootScope, $scope, $timeout, $modal, AppUsersManager, AppChatsManager, AppPhotosManager, MtpApiManager, Storage, NotificationsManager, MtpApiFileManager, ApiUpdatesManager, ChangelogNotifyService, LayoutSwitchService, AppRuntimeManager, ErrorService, _) { .controller('SettingsModalController', function ($rootScope, $scope, $timeout, $modal, AppUsersManager, AppChatsManager, AppPhotosManager, MtpApiManager, Storage, NotificationsManager, MtpApiFileManager, PasswordManager, ApiUpdatesManager, ChangelogNotifyService, LayoutSwitchService, AppRuntimeManager, ErrorService, _) {
$scope.profile = {}; $scope.profile = {};
$scope.photo = {}; $scope.photo = {};
@ -2545,6 +2623,42 @@ angular.module('myApp.controllers', ['myApp.i18n'])
$scope.$watch('photo.file', onPhotoSelected); $scope.$watch('photo.file', onPhotoSelected);
$scope.password = {_: 'account.noPassword'};
updatePasswordState();
var updatePasswordTimeout = false;
$scope.changePassword = function (options) {
options = options || {};
if (options.action == 'cancel_email') {
return ErrorService.confirm({type: 'PASSWORD_ABORT_SETUP'}).then(function () {
PasswordManager.updateSettings($scope.password, {email: ''}).then(updatePasswordState);
});
}
var scope = $rootScope.$new();
scope.password = $scope.password;
angular.extend(scope, options);
var modal = $modal.open({
scope: scope,
templateUrl: templateUrl('password_update_modal'),
controller: 'PasswordUpdateModalController',
windowClass: 'md_simple_modal_window mobile_modal'
});
modal.result['finally'](updatePasswordState);
};
function updatePasswordState () {
$timeout.cancel(updatePasswordTimeout);
updatePasswordTimeout = false;
PasswordManager.getState().then(function (result) {
$scope.password = result;
if (result._ == 'account.noPassword' && result.email_unconfirmed_pattern) {
updatePasswordTimeout = $timeout(updatePasswordState, 5000);
}
});
}
function onPhotoSelected (photo) { function onPhotoSelected (photo) {
if (!photo || !photo.type || photo.type.indexOf('image') !== 0) { if (!photo || !photo.type || photo.type.indexOf('image') !== 0) {
return; return;
@ -2870,6 +2984,144 @@ angular.module('myApp.controllers', ['myApp.i18n'])
}) })
}) })
.controller('PasswordUpdateModalController', function ($scope, $q, _, PasswordManager, MtpApiManager, ErrorService, $modalInstance) {
$scope.passwordSettings = {};
$scope.updatePassword = function () {
delete $scope.passwordSettings.error_field;
var confirmPromise;
if ($scope.action == 'disable') {
confirmPromise = $q.when();
}
else {
if (!$scope.passwordSettings.new_password) {
$scope.passwordSettings.error_field = 'new_password';
$scope.$broadcast('new_password_focus');
return false;
}
if ($scope.passwordSettings.new_password != $scope.passwordSettings.confirm_password) {
$scope.passwordSettings.error_field = 'confirm_password';
$scope.$broadcast('confirm_password_focus');
return false;
}
confirmPromise = $scope.passwordSettings.email
? $q.when()
: ErrorService.confirm({type: 'RECOVERY_EMAIL_EMPTY'});
}
$scope.passwordSettings.loading = true;
confirmPromise.then(function () {
PasswordManager.updateSettings($scope.password, {
cur_password: $scope.passwordSettings.cur_password || '',
new_password: $scope.passwordSettings.new_password,
email: $scope.passwordSettings.email,
hint: $scope.passwordSettings.hint
}).then(function (result) {
delete $scope.passwordSettings.loading;
$modalInstance.close(true);
if ($scope.action == 'disable') {
ErrorService.alert(
_('error_modal_password_disabled_title'),
_('error_modal_password_disabled_descripion')
);
} else {
ErrorService.alert(
_('error_modal_password_success_title'),
_('error_modal_password_success_descripion')
);
}
}, function (error) {
switch (error.type) {
case 'PASSWORD_HASH_INVALID':
case 'NEW_PASSWORD_BAD':
$scope.passwordSettings.error_field = 'cur_password';
error.handled = true;
$scope.$broadcast('cur_password_focus');
break;
case 'NEW_PASSWORD_BAD':
$scope.passwordSettings.error_field = 'new_password';
error.handled = true;
break;
case 'EMAIL_INVALID':
$scope.passwordSettings.error_field = 'email';
error.handled = true;
break;
case 'EMAIL_UNCONFIRMED':
ErrorService.alert(
_('error_modal_email_unconfirmed_title'),
_('error_modal_email_unconfirmed_descripion')
);
$modalInstance.close(true);
error.handled = true;
break;
}
delete $scope.passwordSettings.loading;
});
})
}
switch ($scope.action) {
case 'disable':
$scope.passwordSettings.new_password = '';
break;
case 'create':
onContentLoaded(function () {
$scope.$broadcast('new_password_focus');
});
break;
}
$scope.$watch('passwordSettings.new_password', function (newValue) {
var len = newValue && newValue.length || 0;
if (!len) {
$scope.passwordSettings.hint = '';
}
else if (len <= 3) {
$scope.passwordSettings.hint = '***';
}
else {
$scope.passwordSettings.hint = newValue.charAt(0) + (new Array(len - 1)).join('*') + newValue.charAt(len - 1);
}
$scope.$broadcast('value_updated');
})
})
.controller('PasswordRecoveryModalController', function ($scope, $q, _, PasswordManager, MtpApiManager, ErrorService, $modalInstance) {
$scope.checkCode = function () {
$scope.recovery.updating = true;
PasswordManager.recover($scope.recovery.code, $scope.options).then(function (result) {
ErrorService.alert(
_('error_modal_password_disabled_title'),
_('error_modal_password_disabled_descripion')
);
$modalInstance.close(result);
}, function (error) {
delete $scope.recovery.updating;
switch (error.type) {
case 'CODE_EMPTY':
case 'CODE_INVALID':
$scope.recovery.error_field = 'code';
error.handled = true;
break;
case 'PASSWORD_EMPTY':
case 'PASSWORD_RECOVERY_NA':
case 'PASSWORD_RECOVERY_EXPIRED':
$modalInstance.dismiss();
error.handled = true;
break;
}
});
};
})
.controller('ContactsModalController', function ($scope, $timeout, $modal, $modalInstance, MtpApiManager, AppUsersManager, ErrorService) { .controller('ContactsModalController', function ($scope, $timeout, $modal, $modalInstance, MtpApiManager, AppUsersManager, ErrorService) {
$scope.contacts = []; $scope.contacts = [];
@ -3115,19 +3367,8 @@ angular.module('myApp.controllers', ['myApp.i18n'])
return MtpApiManager.invokeApi('messages.editChatTitle', { return MtpApiManager.invokeApi('messages.editChatTitle', {
chat_id: $scope.chatID, chat_id: $scope.chatID,
title: $scope.group.name title: $scope.group.name
}).then(function (editResult) { }).then(function (updates) {
ApiUpdatesManager.processUpdateMessage({ ApiUpdatesManager.processUpdateMessage(updates);
_: 'updates',
users: editResult.users,
chats: editResult.chats,
seq: 0,
updates: [{
_: 'updateNewMessage',
message: editResult.message,
pts: editResult.pts,
pts_count: editResult.pts_count
}]
});
var peerString = AppChatsManager.getChatString($scope.chatID); var peerString = AppChatsManager.getChatString($scope.chatID);
$rootScope.$broadcast('history_focus', {peerString: peerString}); $rootScope.$broadcast('history_focus', {peerString: peerString});

11
app/js/directives.js

@ -457,6 +457,14 @@ angular.module('myApp.directives', ['myApp.filters'])
templateUrl: templateUrl('message_attach_contact') templateUrl: templateUrl('message_attach_contact')
}; };
}) })
.directive('myMessageWebpage', function() {
return {
scope: {
'webpage': '=myMessageWebpage'
},
templateUrl: templateUrl('message_attach_webpage')
};
})
.directive('myMessagePending', function() { .directive('myMessagePending', function() {
return { return {
templateUrl: templateUrl('message_attach_pending') templateUrl: templateUrl('message_attach_pending')
@ -2834,7 +2842,7 @@ angular.module('myApp.directives', ['myApp.filters'])
}); });
} }
$scope.$on('value_updated', function (event, args) { $scope.$on('value_updated', function () {
setZeroTimeout(function () { setZeroTimeout(function () {
updateHasValueClass(); updateHasValueClass();
}); });
@ -2852,6 +2860,7 @@ angular.module('myApp.directives', ['myApp.filters'])
element.on('keydown', function (event) { element.on('keydown', function (event) {
if (event.keyCode == 13) { if (event.keyCode == 13) {
element.trigger('submit'); element.trigger('submit');
return cancelEvent(event);
} }
}); });
}; };

10
app/js/lib/bin_utils.js

@ -317,6 +317,16 @@ function sha1BytesSync (bytes) {
return bytesFromArrayBuffer(sha1HashSync(bytes)); return bytesFromArrayBuffer(sha1HashSync(bytes));
} }
function sha256HashSync (bytes) {
console.log(dT(), 'SHA-2 hash start', bytes.byteLength || bytes.length);
var hashWords = CryptoJS.SHA256(bytes);
console.log(dT(), 'SHA-2 hash finish');
var hashBytes = bytesFromWords(hashWords);
return hashBytes;
}
function rsaEncrypt (publicKey, bytes) { function rsaEncrypt (publicKey, bytes) {

4
app/js/lib/config.js

File diff suppressed because one or more lines are too long

21
app/js/lib/ng_utils.js

@ -651,6 +651,7 @@ angular.module('izhukov.utils', [])
awaiting = {}, awaiting = {},
webCrypto = Config.Modes.webcrypto && window.crypto && (window.crypto.subtle || window.crypto.webkitSubtle)/* || window.msCrypto && window.msCrypto.subtle*/, webCrypto = Config.Modes.webcrypto && window.crypto && (window.crypto.subtle || window.crypto.webkitSubtle)/* || window.msCrypto && window.msCrypto.subtle*/,
useSha1Crypto = webCrypto && webCrypto.digest !== undefined, useSha1Crypto = webCrypto && webCrypto.digest !== undefined,
useSha256Crypto = webCrypto && webCrypto.digest !== undefined,
finalizeTask = function (taskID, result) { finalizeTask = function (taskID, result) {
var deferred = awaiting[taskID]; var deferred = awaiting[taskID];
if (deferred !== undefined) { if (deferred !== undefined) {
@ -728,6 +729,26 @@ angular.module('izhukov.utils', [])
return sha1HashSync(bytes); return sha1HashSync(bytes);
}); });
}, },
sha256Hash: function (bytes) {
if (useSha256Crypto) {
var deferred = $q.defer(),
bytesTyped = Array.isArray(bytes) ? convertToUint8Array(bytes) : bytes;
// console.log(dT(), 'Native sha1 start');
webCrypto.digest({name: 'SHA-256'}, bytesTyped).then(function (digest) {
// console.log(dT(), 'Native sha1 done');
deferred.resolve(digest);
}, function (e) {
console.error('Crypto digest error', e);
useSha256Crypto = false;
deferred.resolve(sha256HashSync(bytes));
});
return deferred.promise;
}
return $timeout(function () {
return sha256HashSync(bytes);
});
},
aesEncrypt: function (bytes, keyBytes, ivBytes) { aesEncrypt: function (bytes, keyBytes, ivBytes) {
if (naClEmbed) { if (naClEmbed) {
return performTaskWorker('aes-encrypt', { return performTaskWorker('aes-encrypt', {

74
app/js/lib/schema.tl.txt

@ -191,11 +191,7 @@ messages.messagesSlice#b446ae3 count:int messages:Vector<Message> chats:Vector<C
messages.messageEmpty#3f4e0648 = messages.Message; messages.messageEmpty#3f4e0648 = messages.Message;
messages.statedMessages#7d84b48 messages:Vector<Message> chats:Vector<Chat> users:Vector<User> pts:int pts_count:int = messages.StatedMessages; messages.sentMessage#4c3d47f3 id:int date:int media:MessageMedia pts:int pts_count:int = messages.SentMessage;
messages.statedMessage#96240c6a message:Message chats:Vector<Chat> users:Vector<User> pts:int pts_count:int = messages.StatedMessage;
messages.sentMessage#900eac40 id:int date:int pts:int pts_count:int = messages.SentMessage;
messages.chats#64ff9fd5 chats:Vector<Chat> = messages.Chats; messages.chats#64ff9fd5 chats:Vector<Chat> = messages.Chats;
@ -247,7 +243,7 @@ upload.file#96a18d5 type:storage.FileType mtime:int bytes:bytes = upload.File;
dcOption#2ec2a43c id:int hostname:string ip_address:string port:int = DcOption; dcOption#2ec2a43c id:int hostname:string ip_address:string port:int = DcOption;
config#3e6f732a date:int expires:int test_mode:Bool this_dc:int dc_options:Vector<DcOption> chat_size_max:int broadcast_size_max:int online_update_period_ms:int offline_blur_timeout_ms:int offline_idle_timeout_ms:int online_cloud_timeout_ms:int notify_cloud_delay_ms:int notify_default_delay_ms:int chat_big_size:int disabled_features:Vector<DisabledFeature> = Config; config#68bac247 date:int expires:int test_mode:Bool this_dc:int dc_options:Vector<DcOption> chat_size_max:int broadcast_size_max:int forwarded_count_max:int online_update_period_ms:int offline_blur_timeout_ms:int offline_idle_timeout_ms:int online_cloud_timeout_ms:int notify_cloud_delay_ms:int notify_default_delay_ms:int chat_big_size:int disabled_features:Vector<DisabledFeature> = Config;
nearestDc#8e1a1775 country:string this_dc:int nearest_dc:int = NearestDc; nearestDc#8e1a1775 country:string this_dc:int nearest_dc:int = NearestDc;
@ -256,11 +252,7 @@ help.noAppUpdate#c45a6536 = help.AppUpdate;
help.inviteText#18cb9f78 message:string = help.InviteText; help.inviteText#18cb9f78 message:string = help.InviteText;
messages.statedMessagesLinks#51be5d19 messages:Vector<Message> chats:Vector<Chat> users:Vector<User> pts:int pts_count:int links:Vector<contacts.Link> seq:int = messages.StatedMessages; messages.sentMessageLink#35a1a663 id:int date:int media:MessageMedia pts:int pts_count:int links:Vector<contacts.Link> seq:int = messages.SentMessage;
messages.statedMessageLink#948a288 message:Message chats:Vector<Chat> users:Vector<User> pts:int pts_count:int links:Vector<contacts.Link> seq:int = messages.StatedMessage;
messages.sentMessageLink#e923400d id:int date:int pts:int pts_count:int links:Vector<contacts.Link> seq:int = messages.SentMessage;
inputGeoChat#74d456fa chat_id:int access_hash:long = InputGeoChat; inputGeoChat#74d456fa chat_id:int access_hash:long = InputGeoChat;
@ -410,9 +402,6 @@ account.sentChangePhoneCode#a4f58c4c phone_code_hash:string send_call_timeout:in
updateUserPhone#12b9417b user_id:int phone:string = Update; updateUserPhone#12b9417b user_id:int phone:string = Update;
account.noPassword#5770e7a9 new_salt:bytes = account.Password;
account.password#739e5f72 current_salt:bytes new_salt:bytes hint:string = account.Password;
documentAttributeImageSize#6c37c15c w:int h:int = DocumentAttribute; documentAttributeImageSize#6c37c15c w:int h:int = DocumentAttribute;
documentAttributeAnimated#11b58939 = DocumentAttribute; documentAttributeAnimated#11b58939 = DocumentAttribute;
documentAttributeSticker#994c9882 alt:string = DocumentAttribute; documentAttributeSticker#994c9882 alt:string = DocumentAttribute;
@ -440,6 +429,27 @@ contactLinkNone#feedd3ad = ContactLink;
contactLinkHasPhone#268f3f59 = ContactLink; contactLinkHasPhone#268f3f59 = ContactLink;
contactLinkContact#d502c2d0 = ContactLink; contactLinkContact#d502c2d0 = ContactLink;
updateWebPage#2cc36971 webpage:WebPage = Update;
webPageEmpty#eb1477e8 id:long = WebPage;
webPagePending#c586da1c id:long date:int = WebPage;
webPage#a31ea0b5 flags:# id:long url:string display_url:string type:flags.0?string site_name:flags.1?string title:flags.2?string description:flags.3?string photo:flags.4?Photo embed_url:flags.5?string embed_type:flags.5?string embed_width:flags.6?int embed_height:flags.6?int duration:flags.7?int author:flags.8?string = WebPage;
messageMediaWebPage#a32dd600 webpage:WebPage = MessageMedia;
authorization#7bf2e6f6 hash:long flags:int device_model:string platform:string system_version:string api_id:int app_name:string app_version:string date_created:int date_active:int ip:string country:string region:string = Authorization;
account.authorizations#1250abde authorizations:Vector<Authorization> = account.Authorizations;
account.noPassword#96dabc18 new_salt:bytes email_unconfirmed_pattern:string = account.Password;
account.password#7c18141c current_salt:bytes new_salt:bytes hint:string has_recovery:Bool email_unconfirmed_pattern:string = account.Password;
account.passwordSettings#b7b72ab3 email:string = account.PasswordSettings;
account.passwordInputSettings#bcfc532c flags:# new_salt:flags.0?bytes new_password_hash:flags.0?bytes hint:flags.0?string email:flags.1?string = account.PasswordInputSettings;
auth.passwordRecovery#137948a5 email_pattern:string = auth.PasswordRecovery;
---functions--- ---functions---
invokeAfterMsg#cb9f372d {X:Type} msg_id:long query:!X = X; invokeAfterMsg#cb9f372d {X:Type} msg_id:long query:!X = X;
@ -491,16 +501,16 @@ messages.deleteHistory#f4f8fb61 peer:InputPeer offset:int = messages.AffectedHis
messages.deleteMessages#a5f18925 id:Vector<int> = messages.AffectedMessages; messages.deleteMessages#a5f18925 id:Vector<int> = messages.AffectedMessages;
messages.receivedMessages#28abcb68 max_id:int = Vector<int>; messages.receivedMessages#28abcb68 max_id:int = Vector<int>;
messages.setTyping#a3825e50 peer:InputPeer action:SendMessageAction = Bool; messages.setTyping#a3825e50 peer:InputPeer action:SendMessageAction = Bool;
messages.sendMessage#1ca852a1 peer:InputPeer reply_to_msg_id:int message:string random_id:long = messages.SentMessage; messages.sendMessage#9add8f26 flags:# peer:InputPeer reply_to_msg_id:flags.0?int message:string random_id:long = messages.SentMessage;
messages.sendMedia#fcee7fc0 peer:InputPeer reply_to_msg_id:int media:InputMedia random_id:long = messages.StatedMessage; messages.sendMedia#2d7923b1 flags:# peer:InputPeer reply_to_msg_id:flags.0?int media:InputMedia random_id:long = Updates;
messages.forwardMessages#ded42045 peer:InputPeer id:Vector<int> random_id:Vector<long> = messages.StatedMessages; messages.forwardMessages#55e1728d peer:InputPeer id:Vector<int> random_id:Vector<long> = Updates;
messages.getChats#3c6aa187 id:Vector<int> = messages.Chats; messages.getChats#3c6aa187 id:Vector<int> = messages.Chats;
messages.getFullChat#3b831c66 chat_id:int = messages.ChatFull; messages.getFullChat#3b831c66 chat_id:int = messages.ChatFull;
messages.editChatTitle#b4bc68b5 chat_id:int title:string = messages.StatedMessage; messages.editChatTitle#dc452855 chat_id:int title:string = Updates;
messages.editChatPhoto#d881821d chat_id:int photo:InputChatPhoto = messages.StatedMessage; messages.editChatPhoto#ca4c79d8 chat_id:int photo:InputChatPhoto = Updates;
messages.addChatUser#2ee9ee9e chat_id:int user_id:InputUser fwd_limit:int = messages.StatedMessage; messages.addChatUser#f9a0aa09 chat_id:int user_id:InputUser fwd_limit:int = Updates;
messages.deleteChatUser#c3c5cd23 chat_id:int user_id:InputUser = messages.StatedMessage; messages.deleteChatUser#e0611f16 chat_id:int user_id:InputUser = Updates;
messages.createChat#419d9aee users:Vector<InputUser> title:string = messages.StatedMessage; messages.createChat#9cb126e users:Vector<InputUser> title:string = Updates;
updates.getState#edd4882a = updates.State; updates.getState#edd4882a = updates.State;
updates.getDifference#a041495 pts:int date:int qts:int = updates.Difference; updates.getDifference#a041495 pts:int date:int qts:int = updates.Difference;
@ -520,8 +530,8 @@ help.getInviteText#a4a95186 lang_code:string = help.InviteText;
photos.getUserPhotos#b7ee553c user_id:InputUser offset:int max_id:int limit:int = photos.Photos; photos.getUserPhotos#b7ee553c user_id:InputUser offset:int max_id:int limit:int = photos.Photos;
messages.forwardMessage#3f3f4f2 peer:InputPeer id:int random_id:long = messages.StatedMessage; messages.forwardMessage#33963bf9 peer:InputPeer id:int random_id:long = Updates;
messages.sendBroadcast#41bb0972 contacts:Vector<InputUser> message:string media:InputMedia = messages.StatedMessages; messages.sendBroadcast#bf73f4da contacts:Vector<InputUser> random_id:Vector<long> message:string media:InputMedia = Updates;
geochats.getLocated#7f192d8f geo_point:InputGeoPoint radius:int limit:int = geochats.Located; geochats.getLocated#7f192d8f geo_point:InputGeoPoint radius:int limit:int = geochats.Located;
geochats.getRecents#e1427e6f offset:int limit:int = geochats.Messages; geochats.getRecents#e1427e6f offset:int limit:int = geochats.Messages;
@ -574,12 +584,20 @@ contacts.resolveUsername#bf0131c username:string = User;
account.sendChangePhoneCode#a407a8f4 phone_number:string = account.SentChangePhoneCode; account.sendChangePhoneCode#a407a8f4 phone_number:string = account.SentChangePhoneCode;
account.changePhone#70c32edb phone_number:string phone_code_hash:string phone_code:string = User; account.changePhone#70c32edb phone_number:string phone_code_hash:string phone_code:string = User;
account.getPassword#548a30f5 = account.Password;
account.setPassword#dd2a4d8f current_password_hash:bytes new_salt:bytes new_password_hash:bytes hint:string = Bool;
auth.checkPassword#a63011e password_hash:bytes = auth.Authorization;
messages.getStickers#ae22e045 emoticon:string hash:string = messages.Stickers; messages.getStickers#ae22e045 emoticon:string hash:string = messages.Stickers;
messages.getAllStickers#aa3bc868 hash:string = messages.AllStickers; messages.getAllStickers#aa3bc868 hash:string = messages.AllStickers;
account.updateDeviceLocked#38df3532 period:int = Bool; account.updateDeviceLocked#38df3532 period:int = Bool;
messages.getWebPagePreview#25223e24 message:string = MessageMedia;
account.getAuthorizations#e320c158 = account.Authorizations;
account.resetAuthorization#df77f3bc hash:long = Bool;
account.getPassword#548a30f5 = account.Password;
account.getPasswordSettings#bc8d11bb current_password_hash:bytes = account.PasswordSettings;
account.updatePasswordSettings#fa7c4b86 current_password_hash:bytes new_settings:account.PasswordInputSettings = Bool;
auth.checkPassword#a63011e password_hash:bytes = auth.Authorization;
auth.requestPasswordRecovery#d897bc66 = auth.PasswordRecovery;
auth.recoverPassword#4ea56e92 code:string = auth.Authorization;

31
app/js/lib/tl_utils.js

@ -122,6 +122,9 @@ TLSerialization.prototype.storeDouble = function (f) {
TLSerialization.prototype.storeString = function (s, field) { TLSerialization.prototype.storeString = function (s, field) {
this.debug && console.log('>>>', s, (field || '') + ':string'); this.debug && console.log('>>>', s, (field || '') + ':string');
if (s === undefined) {
s = '';
}
var sUTF8 = unescape(encodeURIComponent(s)); var sUTF8 = unescape(encodeURIComponent(s));
this.checkLength(sUTF8.length + 8); this.checkLength(sUTF8.length + 8);
@ -151,6 +154,9 @@ TLSerialization.prototype.storeBytes = function (bytes, field) {
if (bytes instanceof ArrayBuffer) { if (bytes instanceof ArrayBuffer) {
bytes = new Uint8Array(bytes); bytes = new Uint8Array(bytes);
} }
else if (bytes === undefined) {
bytes = [];
}
this.debug && console.log('>>>', bytesToHex(bytes), (field || '') + ':bytes'); this.debug && console.log('>>>', bytesToHex(bytes), (field || '') + ':bytes');
var len = bytes.byteLength || bytes.length; var len = bytes.byteLength || bytes.length;
@ -222,7 +228,17 @@ TLSerialization.prototype.storeMethod = function (methodName, params) {
var self = this; var self = this;
angular.forEach(methodData.params, function (param) { angular.forEach(methodData.params, function (param) {
self.storeObject(params[param.name], param.type, methodName + '[' + param.name + ']'); var type = param.type;
if (type.indexOf('?') !== -1) {
var condType = type.split('?');
var fieldBit = condType[0].split('.');
if (!(params[fieldBit[0]] & (1 << fieldBit[1]))) {
return;
}
type = condType[1];
}
self.storeObject(params[param.name], type, methodName + '[' + param.name + ']');
}); });
return methodData.type; return methodData.type;
@ -230,6 +246,7 @@ TLSerialization.prototype.storeMethod = function (methodName, params) {
TLSerialization.prototype.storeObject = function (obj, type, field) { TLSerialization.prototype.storeObject = function (obj, type, field) {
switch (type) { switch (type) {
case '#':
case 'int': return this.storeInt(obj, field); case 'int': return this.storeInt(obj, field);
case 'long': return this.storeLong(obj, field); case 'long': return this.storeLong(obj, field);
case 'int128': return this.storeIntBytes(obj, 128, field); case 'int128': return this.storeIntBytes(obj, 128, field);
@ -293,7 +310,17 @@ TLSerialization.prototype.storeObject = function (obj, type, field) {
var self = this; var self = this;
angular.forEach(constructorData.params, function (param) { angular.forEach(constructorData.params, function (param) {
self.storeObject(obj[param.name], param.type, field + '[' + predicate + '][' + param.name + ']'); var type = param.type;
if (type.indexOf('?') !== -1) {
var condType = type.split('?');
var fieldBit = condType[0].split('.');
if (!(obj[fieldBit[0]] & (1 << fieldBit[1]))) {
return;
}
type = condType[1];
}
self.storeObject(obj[param.name], type, field + '[' + predicate + '][' + param.name + ']');
}); });
return constructorData.type; return constructorData.type;

47
app/js/locales/en-us.json

@ -54,6 +54,26 @@
"settings_modal_follow_us_twitter": "Follow us on Twitter!", "settings_modal_follow_us_twitter": "Follow us on Twitter!",
"settings_modal_recent_updates": "Recent updates (ver. {version})", "settings_modal_recent_updates": "Recent updates (ver. {version})",
"settings_modal_set_password": "Set Additional Password",
"settings_modal_change_password": "Change password",
"settings_modal_disable_password": "Turn off",
"settings_modal_password_email_pending": "Click the link in {email} to complete Two-Step Verification setup.",
"settings_modal_password_email_pending_cancel": "Abort",
"password_delete_title": "Turn Password Off",
"password_change_title": "Two-Step Verification",
"password_current_placeholder": "Enter current password",
"password_create_placeholder": "Enter a password",
"password_new_placeholder": "Enter new password",
"password_confirm_placeholder": "Re-enter new password",
"password_hint_placeholder": "Enter password hint",
"password_email_placeholder": "Enter recovery e-mail",
"password_create_description": "This password will be required when you log in on a new device in addition to the pin code.",
"password_create_active": "Saving...",
"password_create_submit": "Save",
"password_delete_active": "Deleting...",
"password_delete_submit": "Delete password",
"page_title_pluralize_notifications": "{'0': 'No notifications', 'one': '1 notification', 'other': '{} notifications'}", "page_title_pluralize_notifications": "{'0': 'No notifications', 'one': '1 notification', 'other': '{} notifications'}",
"profile_edit_modal_title": "Edit profile", "profile_edit_modal_title": "Edit profile",
@ -147,6 +167,9 @@
"confirm_modal_migrate_to_https_md": "Telegram Web now supports additional SSL encryption. Would you like to switch to HTTPS?\nThe HTTP version will be disabled soon.", "confirm_modal_migrate_to_https_md": "Telegram Web now supports additional SSL encryption. Would you like to switch to HTTPS?\nThe HTTP version will be disabled soon.",
"confirm_modal_resize_desktop_md": "Would you like to switch to desktop version?", "confirm_modal_resize_desktop_md": "Would you like to switch to desktop version?",
"confirm_modal_resize_mobile_md": "Would you like to switch to mobile version?", "confirm_modal_resize_mobile_md": "Would you like to switch to mobile version?",
"confirm_modal_recovery_email_empty_md": "Warning! Are you sure you don't want to add a password recovery e-mail?\n\nIf you forget your password, you will lose access to your Telegram account",
"confirm_modal_abort_password_setup": "Abort two-step verification setup?",
"confirm_modal_reset_account_md": "Are you sure?\nThis action can not be undone.\n\nYou will lose all your chats and messages, along with any media and files you shared, if you proceed with resetting your account.",
"confirm_modal_are_u_sure": "Are you sure?", "confirm_modal_are_u_sure": "Are you sure?",
"confirm_modal_logout_submit": "Log Out", "confirm_modal_logout_submit": "Log Out",
@ -161,6 +184,7 @@
"confirm_modal_share_video_submit": "Forward video", "confirm_modal_share_video_submit": "Forward video",
"confirm_modal_share_contact_submit": "Send contact", "confirm_modal_share_contact_submit": "Send contact",
"confirm_modal_share_file_submit": "Share file", "confirm_modal_share_file_submit": "Share file",
"confirm_modal_reset_account_submit": "Reset my account",
"contacts_modal_edit_list": "Edit", "contacts_modal_edit_list": "Edit",
"contacts_modal_edit_cancel": "Cancel", "contacts_modal_edit_cancel": "Cancel",
@ -232,7 +256,12 @@
"error_modal_flood_title": "Too fast", "error_modal_flood_title": "Too fast",
"error_modal_internal_title": "Server error", "error_modal_internal_title": "Server error",
"error_modal_alert": "Alert", "error_modal_alert": "Alert",
"error_modal_email_unconfirmed_title": "Almost there!",
"error_modal_email_unconfirmed_descripion": "Please check your e-mail (don't forget the spam folder) to complete Two-Step Verification setup.",
"error_modal_password_success_title": "Success!",
"error_modal_password_disabled_title": "Password deactivated",
"error_modal_media_not_supported_title": "Unsupported media", "error_modal_media_not_supported_title": "Unsupported media",
"error_modal_recovery_na_title": "Sorry",
"error_modal_network_description": "Please check your internet connection.", "error_modal_network_description": "Please check your internet connection.",
"error_modal_firstname_invali_description": "The first name you entered is invalid.", "error_modal_firstname_invali_description": "The first name you entered is invalid.",
@ -258,6 +287,9 @@
"error_modal_internal_description": "Internal server error occured. Please try again later.", "error_modal_internal_description": "Internal server error occured. Please try again later.",
"error_modal_tech_details": "Technical details here", "error_modal_tech_details": "Technical details here",
"error_modal_multiple_open_tabs": "Please close other Telegram app tabs.", "error_modal_multiple_open_tabs": "Please close other Telegram app tabs.",
"error_modal_recovery_na_description": "Since you haven't provided a recovery e-mail when setting up your password, your remaining options are either to remember your password or to reset your account.",
"error_modal_password_success_descripion": "Your password for Two-Step Verification is now active.",
"error_modal_password_disabled_descripion": "You have disabled Two-Step Verification.",
"head_telegram": "Telegram", "head_telegram": "Telegram",
@ -367,6 +399,21 @@
"login_about_desc3_md": "Our {source-link: source code} is open, so everyone can make a contribution.", "login_about_desc3_md": "Our {source-link: source code} is open, so everyone can make a contribution.",
"login_about_intro": "Welcome to the official Telegram web-client.", "login_about_intro": "Welcome to the official Telegram web-client.",
"login_about_learn": "Learn more", "login_about_learn": "Learn more",
"login_password_title": "Password",
"login_password_label": "You have enabled Two-Step Verification, so your account is protected with an additional password.",
"login_password_forgot_link": "Forgot password?",
"login_account_reset": "Reset account",
"login_password": "Your Password",
"login_incorrect_password": "Incorrect password",
"login_checking_password": "Checking",
"login_recovery_title": "Forgot password?",
"login_code_placeholder": "Code",
"login_code_incorrect": "Incorrect code",
"login_recovery_description_md": "We have sent a recovery code to the e-mail you provided:\n\n{email}\n\nPlease check your e-mail and enter the 6-digit code we have sent here.",
"password_recover_active": "Checking...",
"password_recover_submit": "Submit",
"login_controller_unknown_country": "Unknown", "login_controller_unknown_country": "Unknown",

259
app/js/services.js

@ -802,6 +802,7 @@ angular.module('myApp.services', ['myApp.i18n', 'izhukov.utils'])
var pendingByRandomID = {}; var pendingByRandomID = {};
var pendingByMessageID = {}; var pendingByMessageID = {};
var pendingAfterMsgs = {}; var pendingAfterMsgs = {};
var pendingWebPages = {};
var sendFilePromise = $q.when(); var sendFilePromise = $q.when();
var tempID = -1; var tempID = -1;
@ -1397,6 +1398,18 @@ angular.module('myApp.services', ['myApp.i18n', 'izhukov.utils'])
case 'messageMediaAudio': case 'messageMediaAudio':
AppAudioManager.saveAudio(apiMessage.media.audio); AppAudioManager.saveAudio(apiMessage.media.audio);
break; break;
case 'messageMediaWebPage':
var webpage = apiMessage.media.webpage;
if (webpage.photo && webpage.photo._ === 'photo') {
AppPhotosManager.savePhoto(webpage.photo);
} else {
delete webpage.photo;
}
if (pendingWebPages[webpage.id] === undefined) {
pendingWebPages[webpage.id] = {};
}
pendingWebPages[webpage.id][apiMessage.id] = true;
break;
} }
} }
if (apiMessage.action && apiMessage.action._ == 'messageActionChatEditPhoto') { if (apiMessage.action && apiMessage.action._ == 'messageActionChatEditPhoto') {
@ -1465,7 +1478,12 @@ angular.module('myApp.services', ['myApp.i18n', 'izhukov.utils'])
if (pendingAfterMsgs[peerID]) { if (pendingAfterMsgs[peerID]) {
sentRequestOptions.afterMessageID = pendingAfterMsgs[peerID].messageID; sentRequestOptions.afterMessageID = pendingAfterMsgs[peerID].messageID;
} }
var flags = 0;
if (replyToMsgID) {
flags |= 1;
}
MtpApiManager.invokeApi('messages.sendMessage', { MtpApiManager.invokeApi('messages.sendMessage', {
flags: flags,
peer: inputPeer, peer: inputPeer,
message: text, message: text,
random_id: randomID, random_id: randomID,
@ -1473,6 +1491,7 @@ angular.module('myApp.services', ['myApp.i18n', 'izhukov.utils'])
}, sentRequestOptions).then(function (sentMessage) { }, sentRequestOptions).then(function (sentMessage) {
message.date = sentMessage.date; message.date = sentMessage.date;
message.id = sentMessage.id; message.id = sentMessage.id;
message.media = sentMessage.media;
ApiUpdatesManager.processUpdateMessage({ ApiUpdatesManager.processUpdateMessage({
_: 'updates', _: 'updates',
@ -1625,32 +1644,18 @@ angular.module('myApp.services', ['myApp.i18n', 'izhukov.utils'])
{_: 'documentAttributeFilename', file_name: file.name} {_: 'documentAttributeFilename', file_name: file.name}
]}; ]};
} }
var flags = 0;
if (replyToMsgID) {
flags |= 1;
}
MtpApiManager.invokeApi('messages.sendMedia', { MtpApiManager.invokeApi('messages.sendMedia', {
flags: flags,
peer: inputPeer, peer: inputPeer,
media: inputMedia, media: inputMedia,
random_id: randomID, random_id: randomID,
reply_to_msg_id: replyToMsgID reply_to_msg_id: replyToMsgID
}).then(function (statedMessage) { }).then(function (updates) {
message.date = statedMessage.message.date; ApiUpdatesManager.processUpdateMessage(updates);
message.id = statedMessage.message.id;
message.media = statedMessage.message.media;
ApiUpdatesManager.processUpdateMessage({
_: 'updates',
users: statedMessage.users,
chats: statedMessage.chats,
seq: 0,
updates: [{
_: 'updateMessageID',
random_id: randomIDS,
id: statedMessage.message.id
}, {
_: 'updateNewMessage',
message: message,
pts: statedMessage.pts,
pts_count: statedMessage.pts_count
}]
});
}, function (error) { }, function (error) {
if (attachType == 'photo' && if (attachType == 'photo' &&
error.code == 400 && error.code == 400 &&
@ -1697,12 +1702,15 @@ angular.module('myApp.services', ['myApp.i18n', 'izhukov.utils'])
pendingByRandomID[randomIDS] = [peerID, messageID]; pendingByRandomID[randomIDS] = [peerID, messageID];
} }
function sendOther(peerID, inputMedia) { function sendOther(peerID, inputMedia, options) {
options = options || {};
var messageID = tempID--, var messageID = tempID--,
randomID = [nextRandomInt(0xFFFFFFFF), nextRandomInt(0xFFFFFFFF)], randomID = [nextRandomInt(0xFFFFFFFF), nextRandomInt(0xFFFFFFFF)],
randomIDS = bigint(randomID[0]).shiftLeft(32).add(bigint(randomID[1])).toString(), randomIDS = bigint(randomID[0]).shiftLeft(32).add(bigint(randomID[1])).toString(),
historyStorage = historiesStorage[peerID], historyStorage = historiesStorage[peerID],
inputPeer = AppPeersManager.getInputPeerByID(peerID); inputPeer = AppPeersManager.getInputPeerByID(peerID),
replyToMsgID = options.replyToMsgID;
if (historyStorage === undefined) { if (historyStorage === undefined) {
historyStorage = historiesStorage[peerID] = {count: null, history: [], pending: []}; historyStorage = historiesStorage[peerID] = {count: null, history: [], pending: []};
@ -1760,32 +1768,18 @@ angular.module('myApp.services', ['myApp.i18n', 'izhukov.utils'])
} }
message.send = function () { message.send = function () {
var flags = 0;
if (replyToMsgID) {
flags |= 1;
}
MtpApiManager.invokeApi('messages.sendMedia', { MtpApiManager.invokeApi('messages.sendMedia', {
flags: flags,
peer: inputPeer, peer: inputPeer,
media: inputMedia, media: inputMedia,
random_id: randomID, random_id: randomID,
reply_to_msg_id: 0 reply_to_msg_id: replyToMsgID
}).then(function (statedMessage) { }).then(function (updates) {
message.date = statedMessage.message.date; ApiUpdatesManager.processUpdateMessage(updates);
message.id = statedMessage.message.id;
message.media = statedMessage.message.media;
ApiUpdatesManager.processUpdateMessage({
_: 'updates',
users: statedMessage.users,
chats: statedMessage.chats,
seq: 0,
updates: [{
_: 'updateMessageID',
random_id: randomIDS,
id: statedMessage.message.id
}, {
_: 'updateNewMessage',
message: message,
pts: statedMessage.pts,
pts_count: statedMessage.pts_count
}]
});
}, function (error) { }, function (error) {
toggleError(true); toggleError(true);
}); });
@ -1815,24 +1809,8 @@ angular.module('myApp.services', ['myApp.i18n', 'izhukov.utils'])
peer: AppPeersManager.getInputPeerByID(peerID), peer: AppPeersManager.getInputPeerByID(peerID),
id: msgIDs, id: msgIDs,
random_id: randomIDs random_id: randomIDs
}).then(function (statedMessages) { }).then(function (updates) {
var updates = []; ApiUpdatesManager.processUpdateMessage(updates);
angular.forEach(statedMessages.messages, function(apiMessage) {
updates.push({
_: 'updateNewMessage',
message: apiMessage,
pts: statedMessages.pts,
pts_count: statedMessages.pts_count
});
});
ApiUpdatesManager.processUpdateMessage({
_: 'updates',
users: statedMessages.users,
chats: statedMessages.chats,
seq: 0,
updates: updates
});
}); });
}; };
@ -1911,21 +1889,6 @@ angular.module('myApp.services', ['myApp.i18n', 'izhukov.utils'])
return false; return false;
} }
function onStatedMessage (statedMessage) {
ApiUpdatesManager.processUpdateMessage({
_: 'updates',
users: statedMessage.users,
chats: statedMessage.chats,
seq: 0,
updates: [{
_: 'updateNewMessage',
message: statedMessage.message,
pts: statedMessage.pts,
pts_count: statedMessage.pts_count
}]
});
}
function getMessagePeer (message) { function getMessagePeer (message) {
var toID = message.to_id && AppPeersManager.getPeerID(message.to_id) || 0; var toID = message.to_id && AppPeersManager.getPeerID(message.to_id) || 0;
@ -2013,6 +1976,12 @@ angular.module('myApp.services', ['myApp.i18n', 'izhukov.utils'])
{noLinks: true, noLinebreaks: true} {noLinks: true, noLinebreaks: true}
); );
break; break;
case 'messageMediaWebPage':
if (message.media.webpage.photo) {
message.media.webpage.photo = AppPhotosManager.wrapForHistory(message.media.webpage.photo.id, {website: true});
}
break;
} }
} }
else if (message.action) { else if (message.action) {
@ -2535,6 +2504,32 @@ angular.module('myApp.services', ['myApp.i18n', 'izhukov.utils'])
} }
}); });
break; break;
case 'updateWebPage':
console.log('webpage update', update);
var webpage = update.webpage;
if (pendingWebPages[webpage.id] !== undefined) {
var message, historyMessage;
angular.forEach(pendingWebPages[webpage.id], function (t, msgID) {
if (message = messagesStorage[msgID]) {
message.media = {
_: 'messageMediaWebPage',
webpage: webpage
};
}
if (historyMessage = messagesForHistory[msgID]) {
if (webpage.photo) {
AppPhotosManager.savePhoto(webpage.photo);
webpage.photo = AppPhotosManager.wrapForHistory(webpage.photo.id, {website: true});
}
historyMessage.media = {
_: 'messageMediaWebPage',
webpage: webpage
};
}
});
}
break;
} }
}); });
@ -2551,7 +2546,6 @@ angular.module('myApp.services', ['myApp.i18n', 'izhukov.utils'])
sendFile: sendFile, sendFile: sendFile,
sendOther: sendOther, sendOther: sendOther,
forwardMessages: forwardMessages, forwardMessages: forwardMessages,
onStatedMessage: onStatedMessage,
getMessagePeer: getMessagePeer, getMessagePeer: getMessagePeer,
wrapForDialog: wrapForDialog, wrapForDialog: wrapForDialog,
wrapForHistory: wrapForHistory, wrapForHistory: wrapForHistory,
@ -2652,10 +2646,11 @@ angular.module('myApp.services', ['myApp.i18n', 'izhukov.utils'])
return photos[photoID] || {_: 'photoEmpty'}; return photos[photoID] || {_: 'photoEmpty'};
} }
function wrapForHistory (photoID) { function wrapForHistory (photoID, options) {
options = options || {};
var photo = angular.copy(photos[photoID]) || {_: 'photoEmpty'}, var photo = angular.copy(photos[photoID]) || {_: 'photoEmpty'},
width = Math.min(windowW - 80, Config.Mobile ? 210 : 260), width = options.website ? 100 : Math.min(windowW - 80, Config.Mobile ? 210 : 260),
height = Math.min(windowH - 100, Config.Mobile ? 210 : 260), height = options.website ? 100 : Math.min(windowH - 100, Config.Mobile ? 210 : 260),
thumbPhotoSize = choosePhotoSize(photo, width, height), thumbPhotoSize = choosePhotoSize(photo, width, height),
thumb = { thumb = {
placeholder: 'img/placeholders/PhotoThumbConversation.gif', placeholder: 'img/placeholders/PhotoThumbConversation.gif',
@ -4543,6 +4538,106 @@ angular.module('myApp.services', ['myApp.i18n', 'izhukov.utils'])
}) })
.service('PasswordManager', function ($timeout, $q, $rootScope, MtpApiManager, CryptoWorker, MtpSecureRandom) {
return {
check: check,
getState: getState,
requestRecovery: requestRecovery,
recover: recover,
updateSettings: updateSettings
};
function getState (options) {
return MtpApiManager.invokeApi('account.getPassword', {}, options).then(function (result) {
return result;
});
}
function updateSettings (state, settings) {
var currentHashPromise;
var newHashPromise;
var params = {
new_settings: {
_: 'account.passwordInputSettings',
flags: 0,
hint: settings.hint || ''
}
};
if (typeof settings.cur_password === 'string' &&
settings.cur_password.length > 0) {
currentHashPromise = makePasswordHash(state.current_salt, settings.cur_password);
} else {
currentHashPromise = $q.when([]);
}
if (typeof settings.new_password === 'string' &&
settings.new_password.length > 0) {
var saltRandom = new Array(8);
var newSalt = bufferConcat(state.new_salt, saltRandom);
MtpSecureRandom.nextBytes(saltRandom);
newHashPromise = makePasswordHash(newSalt, settings.new_password);
params.new_settings.new_salt = newSalt;
params.new_settings.flags |= 1;
} else {
if (typeof settings.new_password === 'string') {
params.new_settings.flags |= 1;
params.new_settings.new_salt = [];
}
newHashPromise = $q.when([]);
}
if (typeof settings.email === 'string') {
params.new_settings.flags |= 2;
params.new_settings.email = settings.email || '';
}
return $q.all([currentHashPromise, newHashPromise]).then(function (hashes) {
params.current_password_hash = hashes[0];
params.new_settings.new_password_hash = hashes[1];
return MtpApiManager.invokeApi('account.updatePasswordSettings', params);
});
}
function check (state, password, options) {
return makePasswordHash(state.current_salt, password).then(function (passwordHash) {
return MtpApiManager.invokeApi('auth.checkPassword', {
password_hash: passwordHash
}, options);
});
}
function requestRecovery (state, options) {
return MtpApiManager.invokeApi('auth.requestPasswordRecovery', {}, options);
}
function recover (code, options) {
return MtpApiManager.invokeApi('auth.recoverPassword', {
code: code
}, options);
}
function makePasswordHash (salt, password) {
var passwordUTF8 = unescape(encodeURIComponent(password));
var buffer = new ArrayBuffer(passwordUTF8.length);
var byteView = new Uint8Array(buffer);
for (var i = 0, len = passwordUTF8.length; i < len; i++) {
byteView[i] = passwordUTF8.charCodeAt(i);
}
buffer = bufferConcat(bufferConcat(salt, byteView), salt);
return CryptoWorker.sha256Hash(buffer);
}
})
.service('ErrorService', function ($rootScope, $modal, $window) { .service('ErrorService', function ($rootScope, $modal, $window) {

33
app/less/app.less

@ -242,6 +242,9 @@ input[type="number"] {
padding: 0; padding: 0;
margin: 0 0 22px; margin: 0 0 22px;
} }
.md-input-grouped {
margin-bottom: 12px;
}
.md-input-label { .md-input-label {
font-weight: normal; font-weight: normal;
@ -974,6 +977,12 @@ a.tg_radio_on:hover i.icon-radio {
font-size: 13px; font-size: 13px;
line-height: 160%; line-height: 160%;
} }
.login_form_hint {
color: #999;
margin: 0 0 20px;
font-size: 13px;
line-height: 160%;
}
.login_form_messaging { .login_form_messaging {
color: #999; color: #999;
font-size: 13px; font-size: 13px;
@ -1002,6 +1011,15 @@ a.tg_radio_on:hover i.icon-radio {
} }
} }
.login_forgot_button {
text-align: center;
margin: 30px 0 10px;
}
.login_reset_button {
text-align: center;
margin: 10px 0 0;
}
/* IM page start */ /* IM page start */
/* Dialogs list */ /* Dialogs list */
@ -1614,7 +1632,8 @@ img.im_message_document_thumb {
width: 265px; width: 265px;
padding: 0 0 1px; padding: 0 0 1px;
} }
.im_message_document_actions { .im_message_document_actions,
.im_message_website_description {
width: 265px; width: 265px;
} }
.im_message_document_name { .im_message_document_name {
@ -1760,7 +1779,8 @@ img.im_message_document_thumb {
.im_message_document_name_wrap, .im_message_document_name_wrap,
.im_message_upload_progress_wrap, .im_message_upload_progress_wrap,
.im_message_download_progress_wrap, .im_message_download_progress_wrap,
.im_message_document_actions { .im_message_document_actions,
.im_message_website_description {
width: 207px; width: 207px;
} }
.im_message_document_name { .im_message_document_name {
@ -3361,6 +3381,15 @@ a.countries_modal_search_clear {
&:hover { &:hover {
text-decoration: none; text-decoration: none;
} }
&.pull-right {
color: #3a6d99;
}
}
&_text {
display: block;
padding: 4px 0;
color: #777;
} }
&_version { &_version {

6
app/partials/desktop/confirm_modal.html

@ -43,6 +43,9 @@
<div ng-switch-when="MIGRATE_TO_HTTPS" my-i18n="confirm_modal_migrate_to_https_md"></div> <div ng-switch-when="MIGRATE_TO_HTTPS" my-i18n="confirm_modal_migrate_to_https_md"></div>
<div ng-switch-when="SWITCH_DESKTOP_VERSION" my-i18n="confirm_modal_resize_desktop_md"></div> <div ng-switch-when="SWITCH_DESKTOP_VERSION" my-i18n="confirm_modal_resize_desktop_md"></div>
<div ng-switch-when="SWITCH_MOBILE_VERSION" my-i18n="confirm_modal_resize_mobile_md"></div> <div ng-switch-when="SWITCH_MOBILE_VERSION" my-i18n="confirm_modal_resize_mobile_md"></div>
<div ng-switch-when="RECOVERY_EMAIL_EMPTY" my-i18n="confirm_modal_recovery_email_empty_md"></div>
<div ng-switch-when="PASSWORD_ABORT_SETUP" my-i18n="confirm_modal_abort_password_setup"></div>
<div ng-switch-when="RESET_ACCOUNT" my-i18n="confirm_modal_reset_account_md"></div>
<span ng-switch-default ng-switch="message.length > 0"> <span ng-switch-default ng-switch="message.length > 0">
<span ng-switch-when="true" ng-bind="message"></span> <span ng-switch-when="true" ng-bind="message"></span>
<span ng-switch-default my-i18n="confirm_modal_are_u_sure"></span> <span ng-switch-default my-i18n="confirm_modal_are_u_sure"></span>
@ -55,7 +58,7 @@
<button class="btn btn-md" ng-click="$dismiss()"> <button class="btn btn-md" ng-click="$dismiss()">
<span my-i18n="modal_cancel"></span> <span my-i18n="modal_cancel"></span>
</button> </button>
<button class="btn btn-md btn-md-primary" ng-switch="type" ng-click="$close()" my-focused > <button class="btn btn-md btn-md-primary" ng-switch="type" ng-click="$close()" ng-class="{'btn-md-danger': type == 'RESET_ACCOUNT'}" my-focused >
<span ng-switch-when="LOGOUT" my-i18n="confirm_modal_logout_submit"></span> <span ng-switch-when="LOGOUT" my-i18n="confirm_modal_logout_submit"></span>
<span ng-switch-when="HISTORY_FLUSH" my-i18n="confirm_modal_history_flush_submit"></span> <span ng-switch-when="HISTORY_FLUSH" my-i18n="confirm_modal_history_flush_submit"></span>
<span ng-switch-when="FILES_CLIPBOARD_PASTE" my-i18n="confirm_modal_clipboard_files_send_submit"></span> <span ng-switch-when="FILES_CLIPBOARD_PASTE" my-i18n="confirm_modal_clipboard_files_send_submit"></span>
@ -68,6 +71,7 @@
<span ng-switch-when="VIDEO_SHARE_PEER" my-i18n="confirm_modal_share_video_submit"></span> <span ng-switch-when="VIDEO_SHARE_PEER" my-i18n="confirm_modal_share_video_submit"></span>
<span ng-switch-when="SHARE_CONTACT_PEER" my-i18n="confirm_modal_share_contact_submit"></span> <span ng-switch-when="SHARE_CONTACT_PEER" my-i18n="confirm_modal_share_contact_submit"></span>
<span ng-switch-when="EXT_SHARE_PEER" my-i18n="confirm_modal_share_file_submit"></span> <span ng-switch-when="EXT_SHARE_PEER" my-i18n="confirm_modal_share_file_submit"></span>
<span ng-switch-when="RESET_ACCOUNT" my-i18n="confirm_modal_reset_account_submit"></span>
<span ng-switch-default my-i18n="modal_ok"></span> <span ng-switch-default my-i18n="modal_ok"></span>
</button> </button>
</div> </div>

4
app/partials/desktop/error_modal.html

@ -5,6 +5,7 @@
<h4 ng-if="error" class="md_simple_header" ng-switch="error.type"> <h4 ng-if="error" class="md_simple_header" ng-switch="error.type">
<span ng-switch-when="MEDIA_TYPE_NOT_SUPPORTED" my-i18n="error_modal_media_not_supported_title"></span> <span ng-switch-when="MEDIA_TYPE_NOT_SUPPORTED" my-i18n="error_modal_media_not_supported_title"></span>
<span ng-switch-when="USERNAME_NOT_OCCUPIED" my-i18n="error_modal_not_found_title"></span> <span ng-switch-when="USERNAME_NOT_OCCUPIED" my-i18n="error_modal_not_found_title"></span>
<span ng-switch-when="PASSWORD_RECOVERY_NA" my-i18n="error_modal_recovery_na_title"></span>
<span ng-switch-default ng-switch="error.code"> <span ng-switch-default ng-switch="error.code">
<span ng-switch-when="400" my-i18n="error_modal_bad_request_title"></span> <span ng-switch-when="400" my-i18n="error_modal_bad_request_title"></span>
<span ng-switch-when="401" my-i18n="error_modal_unauthorized_title"></span> <span ng-switch-when="401" my-i18n="error_modal_unauthorized_title"></span>
@ -37,6 +38,7 @@
<span ng-switch-when="USERNAME_OCCUPIED" my-i18n="error_modal_username_occupied_description"></span> <span ng-switch-when="USERNAME_OCCUPIED" my-i18n="error_modal_username_occupied_description"></span>
<span ng-switch-when="MEDIA_TYPE_NOT_SUPPORTED" my-i18n="error_modal_media_not_supported_description"></span> <span ng-switch-when="MEDIA_TYPE_NOT_SUPPORTED" my-i18n="error_modal_media_not_supported_description"></span>
<span ng-switch-when="USERNAME_NOT_OCCUPIED" my-i18n="error_modal_username_not_found_description"></span> <span ng-switch-when="USERNAME_NOT_OCCUPIED" my-i18n="error_modal_username_not_found_description"></span>
<span ng-switch-when="PASSWORD_RECOVERY_NA" my-i18n="error_modal_recovery_na_description"></span>
<div ng-switch-default ng-switch="error.code"> <div ng-switch-default ng-switch="error.code">
@ -66,7 +68,7 @@ Stack: {{error.originalError.stack || error.stack}}</textarea>
</div> </div>
<div class="md_simple_modal_footer"> <div class="md_simple_modal_footer">
<button class="btn btn-md btn-md-primary" ng-click="$dismiss()"> <button class="btn btn-md btn-md-primary" ng-click="$dismiss()" my-focused>
<span my-i18n="modal_ok"></span> <span my-i18n="modal_ok"></span>
</button> </button>
</div> </div>

27
app/partials/desktop/login.html

@ -3,7 +3,7 @@
<div class="login_page"> <div class="login_page">
<div class="login_head_wrap clearfix" ng-switch="progress.enabled"> <div class="login_head_wrap clearfix" ng-switch="progress.enabled">
<div ng-switch-when="true" class="login_head_submit_progress"> <div ng-switch-when="true" class="login_head_submit_progress">
<my-i18n ng-if="!credentials.phone_code_hash" msgid="login_generating_key"></my-i18n><my-i18n ng-if="credentials.phone_code_hash &amp;&amp; !credentials.phone_code_valid" msgid="login_checking_code"></my-i18n><my-i18n ng-if="credentials.phone_code_valid &amp;&amp; credentials.phone_unoccupied" msgid="login_signing_up"></my-i18n><span my-loading-dots></span> <my-i18n ng-if="!credentials.phone_code_hash" msgid="login_generating_key"></my-i18n><my-i18n ng-if="credentials.phone_code_hash &amp;&amp; !credentials.phone_code_valid" msgid="login_checking_code"></my-i18n><my-i18n ng-if="credentials.phone_code_valid &amp;&amp; credentials.phone_unoccupied" msgid="login_signing_up"></my-i18n><my-i18n ng-if="credentials.phone_code_valid &amp;&amp; credentials.password_needed" msgid="login_checking_password"></my-i18n><span my-loading-dots></span>
</div> </div>
<div ng-switch-default class="login_head_submit_wrap"> <div ng-switch-default class="login_head_submit_wrap">
<a class="login_head_submit_btn" ng-if="!credentials.phone_code_hash" ng-click="sendCode()"> <a class="login_head_submit_btn" ng-if="!credentials.phone_code_hash" ng-click="sendCode()">
@ -15,6 +15,9 @@
<a class="login_head_submit_btn" ng-if="credentials.phone_code_valid &amp;&amp; credentials.phone_unoccupied" ng-click="logIn(true)"> <a class="login_head_submit_btn" ng-if="credentials.phone_code_valid &amp;&amp; credentials.phone_unoccupied" ng-click="logIn(true)">
<my-i18n msgid="modal_next"></my-i18n><i class="icon icon-next-submit"></i> <my-i18n msgid="modal_next"></my-i18n><i class="icon icon-next-submit"></i>
</a> </a>
<a class="login_head_submit_btn" ng-if="credentials.phone_code_valid &amp;&amp; credentials.password_needed" ng-click="checkPassword()">
<my-i18n msgid="modal_next"></my-i18n><i class="icon icon-next-submit"></i>
</a>
</div> </div>
<a class="login_head_logo_link" href="https://telegram.org" target="_blank"> <a class="login_head_logo_link" href="https://telegram.org" target="_blank">
<i class="icon icon-tg-logo"></i><i class="icon icon-tg-title"></i> <i class="icon icon-tg-logo"></i><i class="icon icon-tg-title"></i>
@ -98,6 +101,28 @@
</form> </form>
<form name="myPasswordForm" ng-if="credentials.phone_code_valid &amp;&amp; credentials.password_needed" ng-submit="checkPassword()">
<h3 class="login_form_head" my-i18n="login_password_title"></h3>
<p class="login_form_lead" my-i18n="login_password_label"></p>
<div class="md-input-group" ng-class="{'md-input-error': error.field == 'password'}" my-labeled-input ng-switch="error.field == 'password'">
<label ng-switch-when="true" class="md-input-label" my-i18n="login_incorrect_password"></label>
<label ng-switch-default class="md-input-label" my-i18n="login_password"></label>
<input autocomplete="off" class="md-input" my-focused name="password" type="password" ng-model="credentials.password" my-submit-on-enter required />
</div>
<p ng-if="password.hint.length > 0" class="login_form_hint" ng-bind="password.hint"></p>
<div class="login_forgot_button">
<button class="btn btn-md" ng-click="forgotPassword($event)" my-i18n="login_password_forgot_link"></button>
</div>
<div ng-if="canReset" class="login_reset_button">
<button class="btn btn-md btn-md-danger" ng-click="resetAccount($event)" my-i18n="login_account_reset"></button>
</div>
</form>
</div> </div>
<div ng-switch="about.shown"> <div ng-switch="about.shown">

8
app/partials/desktop/message.html

@ -50,7 +50,9 @@
</div> </div>
</div> </div>
<div ng-if="::historyMessage.media ? true : false" class="im_message_media" ng-switch="historyMessage.media._"> <div class="im_message_text" ng-if="::historyMessage.message.length || false" ng-bind-html="::historyMessage.richMessage" dir="auto"></div>
<div class="im_message_external_embed_wrap" ng-if="::historyMessage.richUrlEmbed || false" my-external-embed="historyMessage.richUrlEmbed"></div>
<div ng-if="::historyMessage.media || historyMessage.id < 0 ? true : false" class="im_message_media" ng-switch="historyMessage.media._">
<div ng-switch-when="messageMediaPhoto" my-message-photo></div> <div ng-switch-when="messageMediaPhoto" my-message-photo></div>
<div ng-switch-when="messageMediaVideo" my-message-video="historyMessage.media.video" message-id="historyMessage.id"></div> <div ng-switch-when="messageMediaVideo" my-message-video="historyMessage.media.video" message-id="historyMessage.id"></div>
@ -58,6 +60,7 @@
<div ng-switch-when="messageMediaAudio" class="im_message_audio" my-audio-player audio="historyMessage.media.audio"></div> <div ng-switch-when="messageMediaAudio" class="im_message_audio" my-audio-player audio="historyMessage.media.audio"></div>
<div ng-switch-when="messageMediaGeo" my-message-map></div> <div ng-switch-when="messageMediaGeo" my-message-map></div>
<div ng-switch-when="messageMediaContact" class="im_message_contact" my-message-contact></div> <div ng-switch-when="messageMediaContact" class="im_message_contact" my-message-contact></div>
<!-- <div ng-switch-when="messageMediaWebPage" class="im_message_webpage" my-message-webpage="historyMessage.media.webpage"></div> -->
<div ng-switch-when="messageMediaPending" my-message-pending></div> <div ng-switch-when="messageMediaPending" my-message-pending></div>
<div ng-switch-when="messageMediaUnsupported"> <div ng-switch-when="messageMediaUnsupported">
<div class="im_message_text"> <div class="im_message_text">
@ -67,8 +70,7 @@
</div> </div>
<div class="im_message_text" ng-if="::historyMessage.message.length || false" ng-bind-html="::historyMessage.richMessage" dir="auto"></div>
<div class="im_message_external_embed_wrap" ng-if="::historyMessage.richUrlEmbed || false" my-external-embed="historyMessage.richUrlEmbed"></div>
</div> </div>
</div> </div>

21
app/partials/desktop/message_attach_webpage.html

@ -0,0 +1,21 @@
<div class="im_message_document clearfix" ng-class="{im_message_document_thumbed: !!webpage.photo}" ng-if="webpage._ == 'webPage'">
<a ng-if="::webpage.photo" ng-click="docOpen()">
<div class="im_message_document_thumb_wrap">
<img
class="im_message_document_thumb"
my-load-thumb
thumb="webpage.photo.thumb"
/>
</div>
</a>
<div class="im_message_document_info">
<div class="im_message_document_name_wrap">
<a href="" ng-click="docOpen()" class="im_message_document_name" ng-bind="webpage.title || webpage.author"></a>
<span class="im_message_document_size" ng-bind="webpage.display_url"></span>
</div>
<div class="im_message_website_description" ng-bind="webpage.description"></div>
</div>
</div>

32
app/partials/desktop/password_recovery_modal.html

@ -0,0 +1,32 @@
<div class="md_simple_modal_wrap" my-modal-position>
<div class="md_simple_modal_body">
<form class="modal_simple_form" ng-submit="checkCode()">
<h4 my-i18n="login_recovery_title"></h4>
<div class="md_simple_form_description" my-i18n="login_recovery_description_md">
<my-i18n-param name="email">
<strong ng-bind="recovery.email_pattern"></strong>
</my-i18n-param>
</div>
<div class="md-input-group" ng-class="{'md-input-error': recovery.error_field == 'code'}" my-labeled-input ng-switch="recovery.error_field == 'code'">
<label ng-switch-when="true" class="md-input-label" my-i18n="login_code_incorrect"></label>
<label ng-switch-default class="md-input-label" my-i18n="login_code_placeholder"></label>
<input class="md-input" my-focused type="text" ng-model="recovery.code" name="code" my-focused />
</div>
</form>
</div>
<div class="md_simple_modal_footer">
<button class="btn btn-md" ng-click="$dismiss()" my-i18n="modal_cancel"></button>
<button class="btn btn-md btn-md-primary" ng-class="{disabled: recovery.updating}" ng-click="checkCode()" ng-disabled="recovery.updating" ng-bind="recovery.updating ? 'password_recover_active' : 'password_recover_submit' | i18n"></button>
</div>
</div>

54
app/partials/desktop/password_update_modal.html

@ -0,0 +1,54 @@
<div class="md_simple_modal_wrap" my-modal-position>
<div class="md_simple_modal_body">
<form class="modal_simple_form" ng-submit="updatePassword()">
<h4 ng-switch="action">
<my-i18n ng-switch-when="disable" msgid="password_delete_title"></my-i18n>
<my-i18n ng-switch-default msgid="password_change_title"></my-i18n>
</h4>
<div ng-if="password._ != 'account.noPassword'" class="md-input-group" ng-class="{'md-input-error': passwordSettings.error_field == 'cur_password'}" my-labeled-input>
<label class="md-input-label" my-i18n="password_current_placeholder"></label>
<input class="md-input" my-focused type="password" ng-model="passwordSettings.cur_password" name="cur_password" my-focus-on="cur_password_focus" />
</div>
<div ng-if="action != 'disable'" class="md-input-group md-input-grouped" ng-class="{'md-input-error': passwordSettings.error_field == 'new_password'}" my-labeled-input>
<label class="md-input-label" my-i18n="password_new_placeholder"></label>
<input class="md-input" type="password" ng-model="passwordSettings.new_password" name="new_password" my-focus-on="new_password_focus" />
</div>
<div ng-if="action != 'disable'" class="md-input-group" ng-class="{'md-input-error': passwordSettings.confirm_password && passwordSettings.new_password && passwordSettings.confirm_password != passwordSettings.new_password}" my-labeled-input>
<label class="md-input-label" my-i18n="password_confirm_placeholder"></label>
<input class="md-input" type="password" ng-model="passwordSettings.confirm_password" name="confirm_password" my-focus-on="confirm_password_focus" />
</div>
<div ng-if="action != 'disable'" class="md-input-group" my-labeled-input>
<label class="md-input-label" my-i18n="password_hint_placeholder"></label>
<input class="md-input" type="text" ng-model="passwordSettings.hint" name="hint" />
</div>
<div ng-if="action != 'disable'" class="md_simple_form_description" my-i18n="password_create_description"></div>
<div ng-if="action != 'disable'" class="md-input-group" ng-class="{'md-input-error': passwordSettings.error_field == 'email'}" my-labeled-input>
<label class="md-input-label" my-i18n="password_email_placeholder"></label>
<input class="md-input" type="text" ng-model="passwordSettings.email" name="email" />
</div>
</form>
</div>
<div class="md_simple_modal_footer">
<button class="btn btn-md" ng-click="$dismiss()" my-i18n="modal_cancel"></button>
<button class="btn btn-md btn-md-primary" ng-class="{disabled: passwordSettings.updating}" ng-click="updatePassword()" ng-disabled="passwordSettings.updating" ng-switch="action">
<span ng-switch-when="disable" ng-bind="passwordSettings.updating ? 'password_delete_active' : 'password_delete_submit' | i18n"></span>
<span ng-switch-default ng-bind="passwordSettings.updating ? 'password_create_active' : 'password_create_submit' | i18n"></span>
</button>
</div>
</div>

14
app/partials/desktop/settings_modal.html

@ -110,6 +110,20 @@
<div class="md_modal_section_param_name" my-i18n="settings_modal_language"></div> <div class="md_modal_section_param_name" my-i18n="settings_modal_language"></div>
</div> </div>
<div class="md_modal_section_link_wrap">
<a ng-if="password._ == 'account.noPassword' && password.email_unconfirmed_pattern.length" class="md_modal_section_link pull-right" ng-click="changePassword({action: 'cancel_email'})" my-i18n="settings_modal_password_email_pending_cancel">
</a>
<span class="md_modal_section_text" ng-if="password._ == 'account.noPassword' && password.email_unconfirmed_pattern.length" class="md_modal_section_link" my-i18n="settings_modal_password_email_pending">
<my-i18n-param name="email">
<span ng-bind="password.email_unconfirmed_pattern"></span>
</my-i18n-param>
</span>
<a ng-if="password._ == 'account.noPassword' && !password.email_unconfirmed_pattern" class="md_modal_section_link" ng-click="changePassword({action: 'create'})" my-i18n="settings_modal_set_password"></a>
<a ng-if="password._ == 'account.password'" class="md_modal_section_link pull-right" ng-click="changePassword({action: 'disable'})" my-i18n="settings_modal_disable_password"></a>
<a ng-if="password._ == 'account.password'" class="md_modal_section_link" ng-click="changePassword({action: 'change'})" my-i18n="settings_modal_change_password"></a>
</div>
<div class="md_modal_section_link_wrap"> <div class="md_modal_section_link_wrap">
<a class="md_modal_section_link" ng-click="terminateSessions()" my-i18n="settings_modal_terminate_sessions"></a> <a class="md_modal_section_link" ng-click="terminateSessions()" my-i18n="settings_modal_terminate_sessions"></a>
</div> </div>

294
app/vendor/cryptoJS/crypto.js vendored

@ -1912,136 +1912,188 @@ code.google.com/p/crypto-js/wiki/License
/** /*
* Copyright (c) 2012 T. Michael Keesey CryptoJS v3.1.2
* LICENSE: http://opensource.org/licenses/MIT code.google.com/p/crypto-js
*/ (c) 2009-2013 by Jeff Mott. All rights reserved.
var sha1; code.google.com/p/crypto-js/wiki/License
(function (sha1) { */
var POW_2_24 = Math.pow(2, 24); (function (Math) {
var POW_2_32 = Math.pow(2, 32); // Shortcuts
function hex(n) { var C = CryptoJS;
var s = "", v; var C_lib = C.lib;
for(var i = 7; i >= 0; --i) { var WordArray = C_lib.WordArray;
v = (n >>> (i << 2)) & 15; var Hasher = C_lib.Hasher;
s += v.toString(16); var C_algo = C.algo;
}
return s;
};
function toBytes(n) { // Initialization and round constants tables
var b = [], v; var H = [];
for(var i = 3; i >= 0; --i) { var K = [];
v = (n >> (i * 8)) & 255;
b.push(v);
}
return b;
};
function lrot(n, bits) { // Compute constants
return ((n << bits) | (n >>> (32 - bits))); (function () {
} function isPrime(n) {
var Uint32ArrayBigEndian = (function () { var sqrtN = Math.sqrt(n);
function Uint32ArrayBigEndian(length) { for (var factor = 2; factor <= sqrtN; factor++) {
this.bytes = new Uint8Array(length << 2); if (!(n % factor)) {
} return false;
Uint32ArrayBigEndian.prototype.get = function (index) { }
index <<= 2; }
return (this.bytes[index] * POW_2_24) + ((this.bytes[index + 1] << 16) | (this.bytes[index + 2] << 8) | this.bytes[index + 3]);
}; return true;
Uint32ArrayBigEndian.prototype.set = function (index, value) {
var high = Math.floor(value / POW_2_24), rest = value - (high * POW_2_24);
index <<= 2;
this.bytes[index] = high;
this.bytes[index + 1] = rest >> 16;
this.bytes[index + 2] = (rest >> 8) & 255;
this.bytes[index + 3] = rest & 255;
};
return Uint32ArrayBigEndian;
})();
function string2ArrayBuffer(s) {
s = s.replace(/[\u0080-\u07ff]/g, function (c) {
var code = c.charCodeAt(0);
return String.fromCharCode(192 | code >> 6, 128 | code & 63);
});
s = s.replace(/[\u0080-\uffff]/g, function (c) {
var code = c.charCodeAt(0);
return String.fromCharCode(224 | code >> 12, 128 | code >> 6 & 63, 128 | code & 63);
});
var n = s.length, array = new Uint8Array(n);
for(var i = 0; i < n; ++i) {
array[i] = s.charCodeAt(i);
}
return array.buffer;
}
function bytes2ArrayBuffer(b) {
var n = b.length, array = new Uint8Array(n);
for(var i = 0; i < n; ++i) {
array[i] = b[i];
}
return array.buffer;
}
function hash(bufferOrString, byteArray) {
var source;
if (bufferOrString instanceof ArrayBuffer) {
source = bufferOrString;
} else if (Object.prototype.toString.apply(bufferOrString) == '[object Array]') {
source = bytes2ArrayBuffer(bufferOrString);
} else {
source = string2ArrayBuffer(String(bufferOrString));
} }
var h0 = 1732584193, h1 = 4023233417, h2 = 2562383102, h3 = 271733878, h4 = 3285377520, i, sbytes = source.byteLength, sbits = sbytes << 3, minbits = sbits + 65, bits = Math.ceil(minbits / 512) << 9, bytes = bits >>> 3, slen = bytes >>> 2, s = new Uint32ArrayBigEndian(slen), s8 = s.bytes, j, w = new Uint32Array(80), sourceArray = new Uint8Array(source);
for(i = 0; i < sbytes; ++i) { function getFractionalBits(n) {
s8[i] = sourceArray[i]; return ((n - (n | 0)) * 0x100000000) | 0;
} }
s8[sbytes] = 128;
s.set(slen - 2, Math.floor(sbits / POW_2_32)); var n = 2;
s.set(slen - 1, sbits & 4294967295); var nPrime = 0;
for(i = 0; i < slen; i += 16) { while (nPrime < 64) {
for(j = 0; j < 16; ++j) { if (isPrime(n)) {
w[j] = s.get(i + j); if (nPrime < 8) {
} H[nPrime] = getFractionalBits(Math.pow(n, 1 / 2));
for(; j < 80; ++j) { }
w[j] = lrot(w[j - 3] ^ w[j - 8] ^ w[j - 14] ^ w[j - 16], 1); K[nPrime] = getFractionalBits(Math.pow(n, 1 / 3));
nPrime++;
} }
var a = h0, b = h1, c = h2, d = h3, e = h4, f, k, temp;
for(j = 0; j < 80; ++j) { n++;
if(j < 20) { }
f = (b & c) | ((~b) & d); }());
k = 1518500249;
// Reusable object
var W = [];
/**
* SHA-256 hash algorithm.
*/
var SHA256 = C_algo.SHA256 = Hasher.extend({
_doReset: function () {
this._hash = new WordArray.init(H.slice(0));
},
_doProcessBlock: function (M, offset) {
// Shortcut
var H = this._hash.words;
// Working variables
var a = H[0];
var b = H[1];
var c = H[2];
var d = H[3];
var e = H[4];
var f = H[5];
var g = H[6];
var h = H[7];
// Computation
for (var i = 0; i < 64; i++) {
if (i < 16) {
W[i] = M[offset + i] | 0;
} else { } else {
if(j < 40) { var gamma0x = W[i - 15];
f = b ^ c ^ d; var gamma0 = ((gamma0x << 25) | (gamma0x >>> 7)) ^
k = 1859775393; ((gamma0x << 14) | (gamma0x >>> 18)) ^
} else { (gamma0x >>> 3);
if(j < 60) {
f = (b & c) ^ (b & d) ^ (c & d); var gamma1x = W[i - 2];
k = 2400959708; var gamma1 = ((gamma1x << 15) | (gamma1x >>> 17)) ^
} else { ((gamma1x << 13) | (gamma1x >>> 19)) ^
f = b ^ c ^ d; (gamma1x >>> 10);
k = 3395469782;
} W[i] = gamma0 + W[i - 7] + gamma1 + W[i - 16];
}
} }
temp = (lrot(a, 5) + f + e + k + w[j]) & 4294967295;
e = d; var ch = (e & f) ^ (~e & g);
var maj = (a & b) ^ (a & c) ^ (b & c);
var sigma0 = ((a << 30) | (a >>> 2)) ^ ((a << 19) | (a >>> 13)) ^ ((a << 10) | (a >>> 22));
var sigma1 = ((e << 26) | (e >>> 6)) ^ ((e << 21) | (e >>> 11)) ^ ((e << 7) | (e >>> 25));
var t1 = h + sigma1 + ch + K[i] + W[i];
var t2 = sigma0 + maj;
h = g;
g = f;
f = e;
e = (d + t1) | 0;
d = c; d = c;
c = lrot(b, 30); c = b;
b = a; b = a;
a = temp; a = (t1 + t2) | 0;
} }
h0 = (h0 + a) & 4294967295;
h1 = (h1 + b) & 4294967295;
h2 = (h2 + c) & 4294967295;
h3 = (h3 + d) & 4294967295;
h4 = (h4 + e) & 4294967295;
}
if (byteArray) { // Intermediate hash value
return toBytes(h0).concat(toBytes(h1), toBytes(h2), toBytes(h3), toBytes(h4)); H[0] = (H[0] + a) | 0;
H[1] = (H[1] + b) | 0;
H[2] = (H[2] + c) | 0;
H[3] = (H[3] + d) | 0;
H[4] = (H[4] + e) | 0;
H[5] = (H[5] + f) | 0;
H[6] = (H[6] + g) | 0;
H[7] = (H[7] + h) | 0;
},
_doFinalize: function () {
// Shortcuts
var data = this._data;
var dataWords = data.words;
var nBitsTotal = this._nDataBytes * 8;
var nBitsLeft = data.sigBytes * 8;
// Add padding
dataWords[nBitsLeft >>> 5] |= 0x80 << (24 - nBitsLeft % 32);
dataWords[(((nBitsLeft + 64) >>> 9) << 4) + 14] = Math.floor(nBitsTotal / 0x100000000);
dataWords[(((nBitsLeft + 64) >>> 9) << 4) + 15] = nBitsTotal;
data.sigBytes = dataWords.length * 4;
// Hash final blocks
this._process();
// Return final computed hash
return this._hash;
},
clone: function () {
var clone = Hasher.clone.call(this);
clone._hash = this._hash.clone();
return clone;
} }
return hex(h0) + hex(h1) + hex(h2) + hex(h3) + hex(h4); });
}
sha1.hash = hash; /**
})(sha1 || (sha1 = {})); * Shortcut function to the hasher's object interface.
*
* @param {WordArray|string} message The message to hash.
*
* @return {WordArray} The hash.
*
* @static
*
* @example
*
* var hash = CryptoJS.SHA256('message');
* var hash = CryptoJS.SHA256(wordArray);
*/
C.SHA256 = Hasher._createHelper(SHA256);
/**
* Shortcut function to the HMAC's object interface.
*
* @param {WordArray|string} message The message to hash.
* @param {WordArray|string} key The secret key.
*
* @return {WordArray} The HMAC.
*
* @static
*
* @example
*
* var hmac = CryptoJS.HmacSHA256(message, key);
*/
C.HmacSHA256 = Hasher._createHmacHelper(SHA256);
}(Math));

Loading…
Cancel
Save