diff --git a/app/css/app.css b/app/css/app.css index 7ac9490c..42e128ef 100644 --- a/app/css/app.css +++ b/app/css/app.css @@ -79,7 +79,7 @@ input[type="number"] { } .btn-success { color: #ffffff; - background-color: #6AC065; + background-color: #6ec26d; } .btn-success:hover, @@ -94,7 +94,7 @@ input[type="number"] { .btn-success:active, .btn-success.active, .open .dropdown-toggle.btn-success { - background: #5aaf54; + background: #66b864; background-image: none; } @@ -121,7 +121,7 @@ input[type="number"] { .btn-primary { color: #ffffff; - background-color: #5d8db3; + background-color: #6490b1; border-radius: 3px; } .btn-primary:hover, @@ -425,7 +425,7 @@ input[type="number"] { .modal-close-button i { display: inline-block; background: url(../img/icons/IconsetW.png) -15px -320px no-repeat; - background-size: 42px 971px; + background-size: 42px 1171px; width: 12px; height: 12px; margin: 21px; @@ -594,73 +594,31 @@ a.tg_radio_on:hover i.icon-radio { .tg_range_wrap { line-height: 18px; } -input.tg_range { +.tg_slider_wrap { + position: relative; cursor: pointer; - outline: none !important; - -webkit-appearance: none; - width: 100%; - max-width: 362px; - display: inline-block; - background: #c7c7c7; - height: 3px; line-height: 18px; - vertical-align: top; - margin: 8px 0; - border-radius: 2px; + height: 18px; } -input.tg_range::-moz-range-track { - cursor: pointer; - outline: none !important; - -webkit-appearance: none; - width: 100%; - max-width: 362px; - display: inline-block; +.tg_slider_track { + position: absolute; background: #c7c7c7; height: 3px; - line-height: 18px; - vertical-align: top; margin: 8px 0; border-radius: 2px; -} -input.tg_range::-webkit-slider-thumb { - border: 0; - -webkit-appearance: none; - background: #568cb5; - width: 12px; - height: 12px; - border-radius: 6px; - overflow: hidden; -} -input.tg_range::-moz-range-thumb { - border: 0; - background: #568cb5; - width: 12px; - height: 12px; - border-radius: 6px; - overflow: hidden; -} -input.tg_range::-ms-track { - color: transparent; - border: 0; - cursor: pointer; - outline: none !important; width: 100%; - max-width: 362px; - display: inline-block; - background: #c7c7c7; - height: 3px; - line-height: 18px; - vertical-align: top; - margin: 8px 0; - border-radius: 2px; + z-index: 2; } -input.tg_range::-ms-thumb { +.tg_slider_thumb { + position: absolute; border: 0; background: #568cb5; width: 12px; height: 12px; border-radius: 6px; + margin-top: 4px; overflow: hidden; + z-index: 3; } @@ -910,7 +868,7 @@ img.welcome_logo { font-size: 12px; line-height: normal; background: #F2F2F2 url(../img/icons/IconsetW.png) -6px -205px no-repeat; - background-size: 42px 971px; + background-size: 42px 1171px; border: 1px solid #F2F2F2; border-radius: 3px; padding: 6px 20px 6px 30px; @@ -938,7 +896,7 @@ img.welcome_logo { height: 13px; vertical-align: text-top; background: url(../img/icons/IconsetW.png) -15px -192px no-repeat; - background-size: 42px 971px; + background-size: 42px 1171px; opacity: 0.6; } .is_1x .im_dialogs_search_clear { @@ -1070,7 +1028,7 @@ a.im_dialog_selected .im_dialog_message_text { } .im_dialog_badge { - background: #75BB72; + background: #6ec26d; border-radius: 2px; font-size: 10px; padding: 3px 4px; @@ -1144,7 +1102,7 @@ a.im_dialog_selected .im_dialog_date { margin-left: 6px; background: url(../img/icons/IconsetW.png) -17px -444px no-repeat; - background-size: 42px 971px; + background-size: 42px 1171px; } .is_1x .icon-caret { background-image: url(../img/icons/IconsetW_1x.png); @@ -1302,7 +1260,7 @@ div.im_message_video_thumb { height: 42px; background: url(../img/icons/IconsetW.png) 0 -590px no-repeat; - background-size: 42px 971px; + background-size: 42px 1171px; z-index: 1; } .is_1x .icon-videoplay { @@ -1329,26 +1287,37 @@ div.im_message_video_thumb { height: 19px; background: url(../img/icons/IconsetW.png) -14px -389px no-repeat; - background-size: 42px 971px; + background-size: 42px 1171px; } .is_1x .icon-geo-point { background-image: url(../img/icons/IconsetW_1x.png); } -.im_message_iframe_video { - position: relative; - padding-bottom: 56.25%; /* 16/9 ratio */ - height: 0; - overflow: hidden; - margin-top: 5px; +.im_message_media_embed { + position: relative; + height: 0; + overflow: hidden; + margin-top: 5px; } -.im_message_iframe_video iframe, -.im_message_iframe_video webview { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; +.im_message_video_embed { + padding-bottom: 56.25%; /* 16/9 ratio */ +} +.im_message_insta_embed { + padding-bottom: 122%; +} +.im_message_vine_embed { + padding-bottom: 100%; +} +.im_message_media_embed iframe, +.im_message_media_embed webview { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; +} +.im_message_twitter_embed > blockquote { + visibility: hidden; } .im_message_gif_wrap { @@ -1370,42 +1339,70 @@ div.im_message_video_thumb { z-index: 1; } +.im_message_video, .im_message_document, -.im_message_upload_file { +.im_message_upload_file, +.im_message_audio { margin-top: 3px; - border-radius: 3px; - display: inline-block; - width: 340px; + width: 317px; } .im_message_audio { margin-top: 3px; } -.icon-document, -.icon-photo, -.icon-video { +.im_message_file_button { display: block; + background: rgba(218,228,234,0.50); float: left; - width: 38px; - height: 38px; - vertical-align: text-top; - - background: #F2F2F2 url(../img/icons/IconsetW.png) -2px -229px no-repeat; - background-size: 42px 971px; - border-radius: 3px; + width: 42px; + height: 42px; + border-radius: 0; margin-right: 10px; } -.is_1x .icon-document, -.is_1x .icon-photo, -.is_1x .icon-video { +.im_message_file_button_icon { + display: inline-block; + line-height: 0; + /*#dae4ea 50%*/ + background: url(../img/icons/IconsetW.png) -15px -953px no-repeat; + background-size: 42px 1171px; + width: 12px; + height: 20px; + margin: 11px 15px; +} +.is_1x .im_message_file_button_icon { background-image: url(../img/icons/IconsetW_1x.png); } +.im_message_file_button_dl_doc .im_message_file_button_icon { + background-position: -13px -983px; + width: 16px; + height: 18px; + margin: 12px 13px; +} +.im_message_file_button_dl_audio { + background: #6490b1; + border-radius: 2px; +} +.im_message_file_button_dl_audio .im_message_file_button_icon { + display: block; + width: 15px; + height: 18px; + background: url(../img/icons/IconsetW.png) -15px -897px no-repeat; + background-size: 42px 1171px; + margin: 12px 13.5px; +} +.is_1x .im_message_file_button_dl_audio .im_message_file_button_icon { + background-image: url(../img/icons/IconsetW_1x.png); + background-position: -15px -899px; +} +.im_message_file_button_dl_audio .audio_player_btn_icon_pause, +.is_1x .im_message_file_button_dl_audio .audio_player_btn_icon_pause { + width: 12px; + height: 16px; + background-position: -15px -927px; + margin: 13px 15px; +} .im_message_selected .icon-document, -.im_message_selected .icon-photo, -.im_message_selected .icon-video, -.im_history_selectable .im_message_outer_wrap:hover .icon-document, -.im_history_selectable .im_message_outer_wrap:hover .icon-photo, -.im_history_selectable .im_message_outer_wrap:hover .icon-video { +.im_history_selectable .im_message_outer_wrap:hover .icon-document { background-color: #dae6f0; background-position: -2px -542px; } @@ -1440,19 +1437,19 @@ img.im_message_document_thumb { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; - width: 290px; + width: 265px; padding: 0 0 1px; } .im_message_document_actions { - width: 290px; + width: 265px; } .im_message_document_name { - color: #222; + color: #3a6d99; display: inline-block; font-weight: bold; - max-width: 200px; + max-width: 170px; overflow: hidden; vertical-align: text-top; white-space: nowrap; @@ -1461,63 +1458,30 @@ img.im_message_document_thumb { .im_message_document_size { color: #999; padding-left: 2px; + vertical-align: text-top; } .im_message_document_actions a, .audio_player_actions a { margin-right: 10px; } -.audio_player_button { - width: 38px; - height: 38px; - padding-left: 12px; - padding-right: 12px; - border-radius: 3px; - margin-right: 12px; -} -.audio_player_btn_icon { - display: block; - width: 14px; - height: 17px; - background: url(../img/icons/IconsetW.png) -15px -897px no-repeat; - background-size: 42px 971px; -} -.is_1x .audio_player_btn_icon { - background-image: url(../img/icons/IconsetW_1x.png); - background-position: -15px -898px; -} -.audio_player_btn_icon_pause, -.is_1x .audio_player_btn_icon_pause, -.audio_player_btn_icon_cancel, -.is_1x .audio_player_btn_icon_cancel { - width: 13px; - height: 15px; - background-position: -15px -923px; -} -.is_1x .audio_player_btn_icon_pause, -.is_1x .audio_player_btn_icon_cancel { - background-position: -15px -925px; -} .audio_player_title_wrap { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; - padding: 0 0 1px; - line-height: 18px; + padding: 1px 0 1px; + line-height: 16px; height: 19px; + width: 200px; } .audio_player_title { - color: #222; display: inline-block; font-weight: bold; - max-width: 200px; + max-width: 85px; overflow: hidden; vertical-align: text-top; white-space: nowrap; text-overflow: ellipsis; } -.audio_player_title:hover { - color: #222; -} .audio_player_meta { overflow: hidden; vertical-align: text-top; @@ -1528,78 +1492,138 @@ img.im_message_document_thumb { color: #999; padding-left: 2px; } +.audio_player_actions { + margin-top: 3px; +} -.im_message_upload_progress_wrap, -.im_message_download_progress_wrap { - margin-top: 5px; - width: 290px; +.audio_player_seek_slider { + float: left; + margin-right: 15px; + width: 200px; +} +.audio_player_seek_slider .tg_slider_wrap { + height: 16px; + line-height: 16px; +} +.audio_player_seek_slider .tg_slider_thumb { + background: #6490b1; + width: 4px; + height: 16px; + line-height: 16px; + margin-top: 0; + border-radius: 0; +} +.audio_player_seek_slider .tg_slider_track { + margin: 6px 0; + background: rgba(218,228,234,0.50); + height: 4px; + border-radius: 0; +} +.audio_player_seek_slider .tg_slider_track_fill { + background: #6490b1; + height: 4px; + width: 0; +} + +.audio_player_volume_slider { + width: 50px; + float: left; +} +.audio_player_volume_slider .tg_slider_wrap { + height: 16px; + line-height: 16px; +} +.audio_player_volume_slider .tg_slider_thumb { + display: none; + background: #6490b1; + width: 4px; + height: 8px; + line-height: 16px; + margin-top: 4px; + border-radius: 0; +} +.audio_player_volume_slider:hover .tg_slider_thumb { + display: block; +} +.audio_player_volume_slider .tg_slider_track { + margin: 6px 0; + background: rgba(218,228,234,0.50); + height: 4px; +} +.audio_player_volume_slider .tg_slider_track_fill { + background: #6490b1; + height: 4px; + width: 0; } + .audio_player_progress_wrap { - margin-top: 5px; - max-width: 290px; - border-radius: 2px; overflow: hidden; } +.audio_player_progress_wrap .tg_down_progress { + margin-top: 5px; +} + + + + +.im_message_upload_progress_wrap, +.im_message_download_progress_wrap { + margin-top: 5px; + width: 200px; +} .im_message_document_thumbed .im_message_document_name_wrap, .im_message_document_thumbed .im_message_upload_progress_wrap, .im_message_document_thumbed .im_message_download_progress_wrap, .im_message_document_thumbed .im_message_document_actions { - width: 230px; + width: 207px; } .im_message_document_thumbed .im_message_document_name { - max-width: 150px; + max-width: 110px; } .im_message_video .im_message_document_name_wrap, .im_message_video .im_message_download_progress_wrap, .im_message_video .im_message_document_actions { - width: 150px; + width: 152px; } .im_message_video .im_message_document_name_wrap { margin-top: 5px; } +.im_message_cancelable_progress_wrap, +.im_message_playback_progress_wrap { + margin-top: 4px; + /*width: 265px;*/ +} .im_message_media_progress_cancel { - font-size: 11px; - margin-left: 10px; + margin-left: 15px; line-height: 100%; + width: 50px; + display: block; + overflow: hidden; } .tg_up_progress, -.tg_down_progress, -.tg_play_progress { - height: 5px; +.tg_down_progress { + height: 4px; margin: 0; padding: 0; - background: #F2F2F2; + background: rgba(218,228,234,0.50); border: 0; border-radius: 0; -webkit-box-shadow: none; box-shadow: none; } .tg_up_progress .progress-bar, -.tg_down_progress .progress-bar, -.tg_play_progress .progress-bar { - height: 5px; - line-height: 5px; +.tg_down_progress .progress-bar { + height: 4px; + line-height: 4px; background: #6B9ABD; border-radius: 0; overflow: hidden; -webkit-box-shadow: none; box-shadow: none; } -.tg_play_progress { - background: #e4e9ed; - border-radius: 1px; -} -.tg_play_progress .progress-bar { - background: #628fb2; - border-radius: 1px; - /*-webkit-transition: width 1s linear; - transition: width 1s linear;*/ - -webkit-transition: none; - transition: none; -} @@ -1759,7 +1783,7 @@ textarea.im_message_field { height: 23px; vertical-align: text-top; background: url(../img/icons/IconsetW.png) -12px -68px no-repeat; - background-size: 42px 971px; + background-size: 42px 1171px; opacity: 0.8; } .is_1x .icon-paperclip { @@ -1787,7 +1811,7 @@ textarea.im_message_field { height: 23px; vertical-align: text-top; background: url(../img/icons/IconsetW.png) -10px -4px no-repeat; - background-size: 42px 971px; + background-size: 42px 1171px; opacity: 0.8; } .is_1x .icon-emoji { @@ -1836,7 +1860,7 @@ textarea.im_message_field { height: 21px; vertical-align: text-top; background: url(../img/icons/IconsetW.png) -9px -132px no-repeat; - background-size: 42px 971px; + background-size: 42px 1171px; opacity: 0.8; } .is_1x .icon-camera { @@ -1852,7 +1876,7 @@ textarea.im_message_field { .icon-online { - background: #6DBF69; + background: #6ec26d; border: 1px solid #FFF; display: block; width: 11px; @@ -1868,13 +1892,41 @@ textarea.im_message_field { .media_modal_wrap .modal-body { padding: 19px 18px 17px; } -a.img_fullsize { +a.img_fullsize, +.img_fullsize_wrap { display: block; text-align: center; } img.img_fullsize { margin: 0 auto; } +.document_modal_image_wrap { + overflow: auto; +} +.document_fullsize_wrap { + display: none; + cursor: zoom-in; + text-align: center; +} +.document_fullsize_zoomed { + cursor: zoom-out; +} +.document_fullsize_img { + /*max-width: 100%;*/ + -webkit-user-select: none; +} +.document_fullsize_zoomed .document_fullsize_img { + /*min-width: 100%;*/ + -webkit-user-select: none; + image-rendering: optimizeSpeed; /* FUCK SMOOTHING, GIVE ME SPEED */ + image-rendering: -moz-crisp-edges; /* Firefox */ + image-rendering: -o-crisp-edges; /* Opera */ + image-rendering: -webkit-optimize-contrast; /* Chrome (and eventually Safari) */ + image-rendering: optimize-contrast; /* CSS3 Proposed */ + -ms-interpolation-mode: nearest-neighbor; /* IE8+ */ + +} + .media_modal_info { color: #999; margin: 20px 0 0; @@ -1886,8 +1938,12 @@ img.img_fullsize { margin-left: 15px; } .media_modal_author { + color: inherit; font-weight: bold; } +.media_modal_author:hover { + color: inherit; +} .non_osx .media_modal_author { font-size: 12px; } @@ -1914,7 +1970,7 @@ img.img_fullsize { overflow: auto; line-height: 17px; - border: 1px solid #d9dbde; + border: 1px solid #d2dbe3; border-radius: 2px; -webkit-box-shadow: none; box-shadow: none; @@ -2179,7 +2235,7 @@ a:hover .icon-twitter { font-size: 12px; line-height: normal; background: url(../img/icons/IconsetW.png) -6px -205px no-repeat; - background-size: 42px 971px; + background-size: 42px 1171px; border: 1px solid #d9dbde; border-radius: 3px; padding: 6px 15px 6px 30px; @@ -2198,7 +2254,7 @@ a:hover .icon-twitter { height: 13px; vertical-align: text-top; background: url(../img/icons/IconsetW.png) -15px -192px no-repeat; - background-size: 42px 971px; + background-size: 42px 1171px; opacity: 0.6; } .is_1x .contacts_modal_search_clear { @@ -2314,7 +2370,7 @@ img.chat_modal_participant_photo { width: 25px; height: 25px; background: url(../img/icons/IconsetW.png) -9px -516px no-repeat; - background-size: 42px 971px; + background-size: 42px 1171px; opacity: 0.5; } .is_1x .icon-contact-tick { @@ -2386,7 +2442,7 @@ img.chat_modal_participant_photo { .im_message_focus .audio_player_duration, .im_message_focus .audio_player_size, .im_message_focus .im_message_fwd_date { - color: #68839c; + color: #899daf; } .icon-select-tick { @@ -2395,7 +2451,7 @@ img.chat_modal_participant_photo { height: 26px; margin: 13px 0 0 40px; background: url(../img/icons/IconsetW.png) -9px -516px no-repeat; - background-size: 42px 971px; + background-size: 42px 1171px; } .is_1x .icon-select-tick { background-image: url(../img/icons/IconsetW_1x.png); @@ -2514,7 +2570,7 @@ ce671b orange font-size: 12px; line-height: normal; background: #F2F2F2 url(../img/icons/IconsetW.png) -6px -205px no-repeat; - background-size: 42px 971px; + background-size: 42px 1171px; border: 1px solid #F2F2F2; border-radius: 3px; padding: 6px 20px 6px 30px; @@ -2537,7 +2593,7 @@ ce671b orange height: 13px; vertical-align: text-top; background: url(../img/icons/IconsetW.png) -15px -192px no-repeat; - background-size: 42px 971px; + background-size: 42px 1171px; opacity: 0.6; } .is_1x .countries_modal_search_clear { @@ -2707,4 +2763,9 @@ ce671b orange font-size: 14px; line-height: 160%; margin: 25px 0 30px; +} + +#nacl_listener { + position: absolute; + left: -10000px; } \ No newline at end of file diff --git a/app/css/desktop.css b/app/css/desktop.css index ffbcddac..d39a02c9 100644 --- a/app/css/desktop.css +++ b/app/css/desktop.css @@ -179,7 +179,7 @@ a.footer_lang_link.active:active { .im_history_col .nano > .nano-pane, .contacts_modal_col .nano > .nano-pane, .im_dialogs_modal_col .nano > .nano-pane { - background : rgba(3,36,64,0.08); + background : rgba(216,223,225,0.45); /*45% d8dfe5*/ width : 9px; right: 0; top: 0; @@ -218,7 +218,7 @@ a.footer_lang_link.active:active { .im_history_col .nano > .nano-pane > .nano-slider, .contacts_modal_col .nano > .nano-pane > .nano-slider, .im_dialogs_modal_col .nano > .nano-pane > .nano-slider { - background : rgba(3,46,79,0.22); + background : rgba(137,160,179,0.50); /*50% 89a0b3*/ margin: 0; -moz-border-radius : 2px; -webkit-border-radius : 2px; @@ -324,7 +324,7 @@ a.footer_lang_link.active:active { } .icon-message-status { - background: #43A4DB; + background: #6ba2cb; border: 0; display: block; width: 10px; @@ -381,6 +381,7 @@ a.footer_lang_link.active:active { font-size: 13px; line-height: 17px; min-width: 60px; + border-radius: 2px; } .im_message_selected .im_message_date, @@ -393,7 +394,7 @@ a.footer_lang_link.active:active { .im_history_selectable .im_message_outer_wrap:hover .im_message_audio_duration, .im_history_selectable .im_message_outer_wrap:hover .im_message_audio_size, .im_history_selectable .im_message_outer_wrap:hover .im_message_fwd_date { - color: #68839c; + color: #899daf; } .im_content_message_select_area { @@ -492,7 +493,7 @@ a.footer_lang_link.active:active { .im_panel_own_photo { width: 50px; height: 50px; - border-radius: 3px; + border-radius: 0; overflow: hidden; } div.im_panel_peer_photo { @@ -505,7 +506,7 @@ div.im_panel_own_photo { } .im_panel_peer_online { - background: #6DBF69; + background: #6ec26d; border: 1px solid #FFF; display: block; width: 11px; @@ -516,6 +517,10 @@ div.im_panel_own_photo { margin-top: -7px; margin-left: 43px; } +.emoji-wysiwyg-editor, +.im_message_field { + border-radius: 0; +} /* Peer modals */ .user_modal_window .modal-dialog { @@ -787,6 +792,12 @@ div.im_panel_own_photo { display: none; } +.settings_volume_slider { + width: 100%; + max-width: 362px; + display: inline-block; +} + .im_message_selected .im_message_outer_wrap, .im_message_focus .im_message_outer_wrap { diff --git a/app/css/mobile.css b/app/css/mobile.css index f77916b1..77deadb9 100644 --- a/app/css/mobile.css +++ b/app/css/mobile.css @@ -130,7 +130,7 @@ html { vertical-align: text-top; background: url(../img/icons/IconsetW.png) -15px -835px no-repeat; - background-size: 42px 971px; + background-size: 42px 1171px; opacity: 0.8; } .is_1x .icon-back { @@ -535,9 +535,7 @@ img.im_message_video_thumb, color: #93a2ae; } -.im_message_out .icon-document, -.im_message_out .icon-photo, -.im_message_out .icon-video { +.im_message_out .icon-document { background-color: #dae6f0; background-position: -2px -542px; } @@ -961,7 +959,7 @@ a.mobile_modal_action .tg_checkbox_label { .im_submit:active, .im_submit:hover { background: url(../img/icons/IconsetW.png) 2px -860px no-repeat; - background-size: 42px 971px; + background-size: 42px 1171px; color: transparent; box-shadow: none; } @@ -985,7 +983,7 @@ a.mobile_modal_action .tg_checkbox_label { height: 23px; vertical-align: text-top; background: url(../img/icons/IconsetW.png) -12px -68px no-repeat; - background-size: 42px 971px; + background-size: 42px 1171px; opacity: 0.8; } .is_1x .icon-paperclip { @@ -1022,7 +1020,7 @@ a.mobile_modal_action .tg_checkbox_label { opacity: 1; margin: 0; background: url(../img/icons/IconsetW.png) -10px -771px no-repeat; - background-size: 42px 971px; + background-size: 42px 1171px; } .is_1x .icon-emoji { background-image: url(../img/icons/IconsetW_1x.png); @@ -1119,4 +1117,9 @@ a.mobile_modal_action .tg_checkbox_label { } .countries_scrollable_wrap a.countries_modal_country { padding: 8px 8px; +} + +.import_modal_phonebook_wrap { + margin-top: 40px; + text-align: center; } \ No newline at end of file diff --git a/app/img/icons/IconsetW.png b/app/img/icons/IconsetW.png index 3b2b0cd4..39e592bf 100644 Binary files a/app/img/icons/IconsetW.png and b/app/img/icons/IconsetW.png differ diff --git a/app/img/icons/IconsetW_1x.png b/app/img/icons/IconsetW_1x.png index f1fa3182..92d5ec04 100644 Binary files a/app/img/icons/IconsetW_1x.png and b/app/img/icons/IconsetW_1x.png differ diff --git a/app/index.html b/app/index.html index 7d437a38..2e4a48d3 100644 --- a/app/index.html +++ b/app/index.html @@ -56,8 +56,10 @@ + + diff --git a/app/js/controllers.js b/app/js/controllers.js index 47ab1d5e..e147cf98 100644 --- a/app/js/controllers.js +++ b/app/js/controllers.js @@ -316,7 +316,7 @@ angular.module('myApp.controllers', ['myApp.i18n']) ChangelogNotifyService.checkUpdate(); }) - .controller('AppIMController', function ($scope, $location, $routeParams, $modal, $rootScope, $modalStack, MtpApiManager, AppUsersManager, ContactsSelectService, ChangelogNotifyService, ErrorService, AppRuntimeManager) { + .controller('AppIMController', function ($scope, $location, $routeParams, $modal, $rootScope, $modalStack, MtpApiManager, AppUsersManager, AppChatsManager, ContactsSelectService, ChangelogNotifyService, ErrorService, AppRuntimeManager) { $scope.$on('$routeUpdate', updateCurDialog); @@ -400,9 +400,9 @@ angular.module('myApp.controllers', ['myApp.i18n']) $scope.showPeerInfo = function () { if ($scope.curDialog.peerID > 0) { - $rootScope.openUser($scope.curDialog.peerID) + AppUsersManager.openUser($scope.curDialog.peerID) } else if ($scope.curDialog.peerID < 0) { - $rootScope.openChat(-$scope.curDialog.peerID) + AppChatsManager.openChat(-$scope.curDialog.peerID) } }; @@ -467,7 +467,6 @@ angular.module('myApp.controllers', ['myApp.i18n']) peersInDialogs = {}, contactsShown; - MtpApiManager.invokeApi('account.updateStatus', {offline: false}); $scope.$on('dialogs_need_more', function () { // console.log('on need more'); showMoreDialogs(); @@ -629,7 +628,8 @@ angular.module('myApp.controllers', ['myApp.i18n']) if (error.code == 401) { MtpApiManager.logOut()['finally'](function () { - $location.url('/login'); + location.hash = '/login'; + AppRuntimeManager.reload(); }); error.handled = true; } @@ -1765,6 +1765,7 @@ angular.module('myApp.controllers', ['myApp.i18n']) }) .controller('VideoModalController', function ($scope, $rootScope, $modalInstance, PeersSelectService, AppMessagesManager, AppVideoManager, AppPeersManager, ErrorService) { + $scope.video = AppVideoManager.wrapForFull($scope.videoID); $scope.progress = {enabled: false}; @@ -1799,6 +1800,38 @@ angular.module('myApp.controllers', ['myApp.i18n']) }); }) + .controller('DocumentModalController', function ($scope, $rootScope, $modalInstance, PeersSelectService, AppMessagesManager, AppDocsManager, AppPeersManager, ErrorService) { + + $scope.document = AppDocsManager.wrapForHistory($scope.docID); + + $scope.forward = function () { + var messageID = $scope.messageID; + PeersSelectService.selectPeer({confirm_type: 'FORWARD_PEER'}).then(function (peerString) { + var peerID = AppPeersManager.getPeerID(peerString); + AppMessagesManager.forwardMessages(peerID, [messageID]).then(function () { + $rootScope.$broadcast('history_focus', {peerString: peerString}); + }); + }); + }; + + $scope['delete'] = function () { + var messageID = $scope.messageID; + ErrorService.confirm({type: 'MESSAGE_DELETE'}).then(function () { + AppMessagesManager.deleteMessages([messageID]); + }); + }; + + $scope.download = function () { + AppDocsManager.saveDocFile($scope.docID); + }; + + $scope.$on('history_delete', function (e, historyUpdate) { + if (historyUpdate.msgs[$scope.messageID]) { + $modalInstance.dismiss(); + } + }); + }) + .controller('UserModalController', function ($scope, $location, $rootScope, $modal, AppUsersManager, MtpApiManager, NotificationsManager, AppPhotosManager, AppMessagesManager, AppPeersManager, PeersSelectService, ErrorService) { var peerString = AppUsersManager.getUserString($scope.userID); @@ -2087,7 +2120,7 @@ angular.module('myApp.controllers', ['myApp.i18n']) } }); - $scope.notify = {}; + $scope.notify = {volume: 0.5}; $scope.send = {}; $scope.$watch('photo.file', onPhotoSelected); @@ -2177,31 +2210,30 @@ angular.module('myApp.controllers', ['myApp.i18n']) if (settings[1]) { $scope.notify.volume = 0; } else if (settings[3] !== false) { - $scope.notify.volume = settings[3] > 0 && Math.ceil(settings[3] * 10) || 0; + $scope.notify.volume = settings[3] > 0 && settings[3] <= 1.0 ? settings[3] : 0; } else { - $scope.notify.volume = 5; + $scope.notify.volume = 0.5; } $scope.notify.canVibrate = NotificationsManager.getVibrateSupport(); $scope.notify.vibrate = !settings[4]; $scope.notify.volumeOf4 = function () { - return 1 + Math.ceil(($scope.notify.volume - 1) / 3.3); + return 1 + Math.ceil(($scope.notify.volume - 0.1) / 0.33); }; $scope.toggleSound = function () { if ($scope.notify.volume) { $scope.notify.volume = 0; } else { - $scope.notify.volume = 5; + $scope.notify.volume = 0.5; } } var testSoundPromise; $scope.$watch('notify.volume', function (newValue, oldValue) { if (newValue !== oldValue) { - var storeVolume = newValue / 10; - Storage.set({notify_volume: storeVolume}); + Storage.set({notify_volume: newValue}); Storage.remove('notify_nosound'); NotificationsManager.clear(); @@ -2209,7 +2241,7 @@ angular.module('myApp.controllers', ['myApp.i18n']) $timeout.cancel(testSoundPromise); } testSoundPromise = $timeout(function () { - NotificationsManager.testSound(storeVolume); + NotificationsManager.testSound(newValue); }, 500); } }); diff --git a/app/js/directives.js b/app/js/directives.js index 9dff598f..c7761ea7 100644 --- a/app/js/directives.js +++ b/app/js/directives.js @@ -177,19 +177,43 @@ angular.module('myApp.directives', ['myApp.filters']) templateUrl: templateUrl('message_attach_photo') }; }) - .directive('myMessageVideo', function() { + .directive('myMessageVideo', function(AppVideoManager) { return { - templateUrl: templateUrl('message_attach_video') - }; - }) - .directive('myMessageDocument', function() { - return { - templateUrl: templateUrl('message_attach_document') + scope: { + 'video': '=myMessageVideo', + 'messageId': '=messageId' + }, + templateUrl: templateUrl('message_attach_video'), + link: function ($scope, element, attrs) { + AppVideoManager.updateVideoDownloaded($scope.video.id); + $scope.videoSave = function () { + AppVideoManager.saveVideoFile($scope.video.id); + }; + $scope.videoOpen = function () { + AppVideoManager.openVideo($scope.video.id, $scope.messageId); + }; + } }; }) - .directive('myMessageAudio', function() { + .directive('myMessageDocument', function(AppDocsManager) { return { - templateUrl: templateUrl('message_attach_audio') + scope: { + 'document': '=myMessageDocument', + 'messageId': '=messageId' + }, + templateUrl: templateUrl('message_attach_document'), + link: function ($scope, element, attrs) { + AppDocsManager.updateDocDownloaded($scope.document.id); + $scope.docSave = function () { + AppDocsManager.saveDocFile($scope.document.id); + }; + $scope.docOpen = function () { + if (!$scope.document.withPreview) { + return $scope.download(); + } + AppDocsManager.openDoc($scope.document.id, $scope.messageId); + }; + } }; }) .directive('myMessageMap', function() { @@ -868,7 +892,7 @@ angular.module('myApp.directives', ['myApp.filters']) }) - .directive('mySendForm', function ($timeout, $modalStack, Storage, ErrorService, $interpolate) { + .directive('mySendForm', function ($timeout, $modalStack, $http, $interpolate, Storage, ErrorService) { return { link: link, @@ -1055,10 +1079,12 @@ angular.module('myApp.directives', ['myApp.filters']) } function onPastedImageEvent (e) { - var element = e && e.target; - var src; - if (element && (src = element.src) && src.indexOf('data') === 0) { - element.parentNode.removeChild(element); + var element = (e.originalEvent || e).target, + src = (element || {}).src || '', + remove = false; + + if (src.substr(0, 5) == 'data:') { + remove = true; src = src.substr(5).split(';'); var contentType = src[0]; var base64 = atob(src[1].split(',')[1]); @@ -1074,6 +1100,15 @@ angular.module('myApp.directives', ['myApp.filters']) $scope.draftMessage.files = [blob]; $scope.draftMessage.isMedia = true; }); + setZeroTimeout(function () { + element.parentNode.removeChild(element); + }) + } + else if (src && !src.match(/img\/blank\.gif/)) { + var replacementNode = document.createTextNode(' ' + src + ' '); + setTimeout(function () { + element.parentNode.replaceChild(replacementNode, element); + }, 100); } }; @@ -1316,7 +1351,7 @@ angular.module('myApp.directives', ['myApp.filters']) }) - .directive('myLoadVideo', function($sce, MtpApiFileManager, _) { + .directive('myLoadVideo', function($sce, AppVideoManager, _) { return { link: link, @@ -1329,37 +1364,12 @@ angular.module('myApp.directives', ['myApp.filters']) function link ($scope, element, attrs) { - $scope.progress = {enabled: true, percent: 1}; - $scope.player = {}; - - var inputLocation = { - _: 'inputVideoFileLocation', - id: $scope.video.id, - access_hash: $scope.video.access_hash - }; + var downloadPromise = AppVideoManager.downloadVideo($scope.video.id); - var hasQt = false, i; - if (navigator.plugins) { - for (i = 0; i < navigator.plugins.length; i++) { - if (navigator.plugins[i].name.indexOf('QuickTime') >= 0) { - hasQt = true; - } - } - } - - var downloadPromise = MtpApiFileManager.downloadFile($scope.video.dc_id, inputLocation, $scope.video.size, {mime: 'video/mp4'}); - - downloadPromise.then(function (url) { - $scope.progress.enabled = false; - // $scope.progress = {enabled: true, percent: 50}; - $scope.player.hasQuicktime = hasQt; - $scope.player.quicktime = false; - $scope.player.src = $sce.trustAsResourceUrl(url); + downloadPromise.then(function () { $scope.$emit('ui_height'); }, function (e) { console.log('Download video failed', e, $scope.video); - $scope.progress.enabled = false; - $scope.player.src = ''; if (e && e.type == 'FS_BROWSER_UNSUPPORTED') { $scope.error = {html: _('error_browser_no_local_file_system_video_md', { @@ -1371,8 +1381,6 @@ angular.module('myApp.directives', ['myApp.filters']) $scope.error = {text: _('error_video_download_failed'), error: e}; } - }, function (progress) { - $scope.progress.percent = Math.max(1, Math.floor(100 * progress.done / progress.total)); }); $scope.$emit('ui_height'); @@ -1384,7 +1392,7 @@ angular.module('myApp.directives', ['myApp.filters']) }) - .directive('myLoadGif', function($rootScope, MtpApiFileManager) { + .directive('myLoadGif', function(AppDocsManager) { return { link: link, @@ -1396,21 +1404,13 @@ angular.module('myApp.directives', ['myApp.filters']) function link ($scope, element, attrs) { - var downloadPromise = false, - inputFileLocation = { - _: 'inputDocumentFileLocation', - id: $scope.document.id, - access_hash: $scope.document.access_hash - }; + var downloadPromise = false; $scope.isActive = false; - $scope.document.url = MtpApiFileManager.getCachedFile(inputFileLocation); - - /*return $scope.document.progress = {enabled: true, percent: 30, total: $scope.document.size};*/ $scope.toggle = function (e) { if (checkClick(e, true)) { - $rootScope.downloadDoc($scope.document.id); + AppDocsManager.saveDocFile($scope.document.id); return false; } @@ -1426,33 +1426,113 @@ angular.module('myApp.directives', ['myApp.filters']) return; } - $scope.document.progress = {enabled: true, percent: 1, total: $scope.document.size}; - - downloadPromise = MtpApiFileManager.downloadFile( - $scope.document.dc_id, - inputFileLocation, - $scope.document.size, - null, - {mime: $scope.document.mime_type} - ); + downloadPromise = AppDocsManager.downloadDoc($scope.document.id); - downloadPromise.then(function (url) { - $scope.document.url = url; + downloadPromise.then(function () { $scope.isActive = true; - delete $scope.document.progress; - console.log('file save done'); $scope.$emit('ui_height'); - }, function () { - $scope.document.progress.enabled = false; - }, function (progress) { - console.log('dl progress', progress); - $scope.document.progress.done = progress.done; - $scope.document.progress.percent = Math.max(1, Math.floor(100 * progress.done / progress.total)); }) } } }) + .directive('myLoadDocument', function(MtpApiFileManager, AppDocsManager) { + + return { + link: link, + templateUrl: templateUrl('full_document'), + scope: { + document: '=myLoadDocument' + } + }; + + function updateModalWidth(element, width) { + while (element && !$(element).hasClass('modal-dialog')) { + element = element.parentNode; + } + if (element) { + $(element).width(width + (Config.Mobile ? 0 : 36)); + } + } + + function link ($scope, element, attrs) { + var loaderWrap = $('.document_fullsize_with_progress_wrap', element); + var fullSizeWrap = $('.document_fullsize_wrap', element); + var fullSizeImage = $('.document_fullsize_img', element); + + var fullWidth = $(window).width() - (Config.Mobile ? 20 : 36); + var fullHeight = $(window).height() - 150; + + $scope.imageWidth = fullWidth; + $scope.imageHeight = fullHeight; + + var thumbPhotoSize = $scope.document.thumb; + + if (thumbPhotoSize && thumbPhotoSize._ != 'photoSizeEmpty') { + var wh = calcImageInBox(thumbPhotoSize.width, thumbPhotoSize.height, fullWidth, fullHeight); + $scope.imageWidth = wh.w; + $scope.imageHeight = wh.h; + + $scope.thumbSrc = MtpApiFileManager.getCachedFile(thumbPhotoSize.location); + } + + $scope.frameWidth = Math.max($scope.imageWidth, Math.min(600, fullWidth)) + $scope.frameHeight = $scope.imageHeight; + + onContentLoaded(function () { + $scope.$emit('ui_height'); + }); + + updateModalWidth(element[0], $scope.frameWidth); + + var checkSizesInt; + var realImageWidth, realImageHeight; + AppDocsManager.downloadDoc($scope.document.id).then(function (url) { + var image = new Image(); + var limit = 100; // 2 sec + var checkSizes = function (e) { + if ((!image.height || !image.width) && --limit) { + return; + } + realImageWidth = image.width; + realImageHeight = image.height; + clearInterval(checkSizesInt); + + var defaultWh = calcImageInBox(image.width, image.height, fullWidth, fullHeight, true); + var zoomedWh = {w: realImageWidth, h: realImageHeight}; + if (defaultWh.w >= zoomedWh.w && defaultWh.h >= zoomedWh.h) { + zoomedWh.w *= 4; + zoomedWh.h *= 4; + } + + var zoomed = true; + $scope.toggleZoom = function () { + zoomed = !zoomed; + var imageWidth = (zoomed ? zoomedWh : defaultWh).w; + var imageHeight = (zoomed ? zoomedWh : defaultWh).h; + fullSizeImage.css({ + width: imageWidth, + height: imageHeight, + marginTop: $scope.frameHeight > imageHeight ? Math.floor(($scope.frameHeight - imageHeight) / 2) : 0 + }); + fullSizeWrap.toggleClass('document_fullsize_zoomed', zoomed); + }; + + $scope.toggleZoom(false); + + fullSizeImage.attr('src', url); + loaderWrap.hide(); + fullSizeWrap.css({width: $scope.frameWidth, height: $scope.frameHeight}).show(); + + }; + checkSizesInt = setInterval(checkSizes, 20); + image.onload = checkSizes; + image.src = url; + setZeroTimeout(checkSizes); + }); + } + }) + .directive('myMapPoint', function(ExternalResourcesManager) { return { @@ -1719,8 +1799,8 @@ angular.module('myApp.directives', ['myApp.filters']) var updateMargin = function () { var height = element[0].offsetHeight, fullHeight = height - (height && usePadding ? 2 * prevMargin : 0), - contHeight = $($window).height(), ratio = attrs.myVerticalPosition && parseFloat(attrs.myVerticalPosition) || 0.5, + contHeight = attrs.contHeight ? $scope.$eval(attrs.contHeight) : $($window).height(), margin = fullHeight < contHeight ? parseInt((contHeight - fullHeight) * ratio) : '', styles = usePadding ? {paddingTop: margin, paddingBottom: margin} @@ -1736,9 +1816,10 @@ angular.module('myApp.directives', ['myApp.filters']) prevMargin = margin; }; + $($window).on('resize', updateMargin); + onContentLoaded(updateMargin); - $($window).on('resize', updateMargin); $scope.$on('ui_height', function () { onContentLoaded(updateMargin); @@ -1751,7 +1832,7 @@ angular.module('myApp.directives', ['myApp.filters']) }) - .directive('myUserLink', function ($timeout, $rootScope, AppUsersManager) { + .directive('myUserLink', function ($timeout, AppUsersManager) { return { link: link @@ -1767,7 +1848,7 @@ angular.module('myApp.directives', ['myApp.filters']) if (element[0].tagName == 'A') { element.on('click', function () { - $rootScope.openUser(userID, attrs.userOverride && $scope.$eval(attrs.userOverride)); + AppUsersManager.openUser(userID, attrs.userOverride && $scope.$eval(attrs.userOverride)); }); } if (attrs.color && $scope.$eval(attrs.color)) { @@ -1821,7 +1902,7 @@ angular.module('myApp.directives', ['myApp.filters']) }) - .directive('myUserPhotolink', function ($rootScope, AppUsersManager) { + .directive('myUserPhotolink', function (AppUsersManager) { return { link: link, @@ -1840,7 +1921,7 @@ angular.module('myApp.directives', ['myApp.filters']) if (element[0].tagName == 'A') { element.on('click', function (e) { - $rootScope.openUser($scope.userID, attrs.userOverride && $scope.$eval(attrs.userOverride)); + AppUsersManager.openUser($scope.userID, attrs.userOverride && $scope.$eval(attrs.userOverride)); }); } @@ -1851,9 +1932,16 @@ angular.module('myApp.directives', ['myApp.filters']) } }) - .directive('myAudioPlayer', function ($sce, $timeout, $q, FileManager, MtpApiFileManager) { + .directive('myAudioPlayer', function ($timeout, $q, Storage, AppAudioManager, AppDocsManager) { var currentPlayer = false; + var audioVolume = 0.5; + + Storage.get('audio_volume').then(function (newAudioVolume) { + if (newAudioVolume >= 0.0 && newAudioVolume <= 1.0) { + audioVolume = newAudioVolume; + } + }); return { link: link, @@ -1863,33 +1951,6 @@ angular.module('myApp.directives', ['myApp.filters']) templateUrl: templateUrl('audio_player') }; - function downloadAudio (audio) { - var inputFileLocation = { - _: audio._ == 'document' ? 'inputDocumentFileLocation' : 'inputAudioFileLocation', - id: audio.id, - access_hash: audio.access_hash - }; - - audio.progress = {enabled: true, percent: 1, total: audio.size}; - - var downloadPromise = MtpApiFileManager.downloadFile(audio.dc_id, inputFileLocation, audio.size, {mime: 'audio/ogg'}); - - audio.progress.cancel = downloadPromise.cancel; - - return downloadPromise.then(function (url) { - delete audio.progress; - audio.rawUrl = url; - audio.url = $sce.trustAsResourceUrl(url); - }, function (e) { - console.log('audio download failed', e); - audio.progress.enabled = false; - }, function (progress) { - console.log('audio dl progress', progress); - audio.progress.done = progress.done; - audio.progress.percent = Math.max(1, Math.floor(100 * progress.done / progress.total)); - }); - } - function checkPlayer (newPlayer) { if (newPlayer === currentPlayer) { return false; @@ -1901,14 +1962,21 @@ angular.module('myApp.directives', ['myApp.filters']) } function link($scope, element, attrs) { + if ($scope.audio._ == 'audio') { + AppAudioManager.updateAudioDownloaded($scope.audio.id); + } else { + AppDocsManager.updateDocDownloaded($scope.audio.id); + } + + $scope.volume = audioVolume; $scope.mediaPlayer = {}; $scope.download = function () { - ($scope.audio.rawUrl ? $q.when() : downloadAudio($scope.audio)).then( - function () { - FileManager.download($scope.audio.rawUrl, $scope.audio.mime_type || 'audio/ogg', $scope.audio.file_name || 'audio.ogg'); - } - ); + if ($scope.audio._ == 'audio') { + AppAudioManager.saveAudioFile($scope.audio.id); + } else { + AppDocsManager.saveDocFile($scope.audio.id); + } }; $scope.togglePlay = function () { @@ -1917,16 +1985,139 @@ angular.module('myApp.directives', ['myApp.filters']) $scope.mediaPlayer.player.playPause(); } else if ($scope.audio.progress && $scope.audio.progress.enabled) { - $scope.audio.progress.cancel(); + return; } else { - downloadAudio($scope.audio).then(function () { + var downloadPromise; + if ($scope.audio._ == 'audio') { + downloadPromise = AppAudioManager.downloadAudio($scope.audio.id); + } else { + downloadPromise = AppDocsManager.downloadDoc($scope.audio.id); + } + + downloadPromise.then(function () { onContentLoaded(function () { checkPlayer($scope.mediaPlayer.player); + $scope.mediaPlayer.player.setVolume(audioVolume); $scope.mediaPlayer.player.play(); }) }) } }; + + $scope.seek = function (position) { + if ($scope.mediaPlayer && $scope.mediaPlayer.player) { + $scope.mediaPlayer.player.seek(position); + } else { + $scope.togglePlay(); + } + }; + $scope.setVolume = function (volume) { + audioVolume = volume; + Storage.set({audio_volume: volume}); + if ($scope.mediaPlayer && $scope.mediaPlayer.player) { + $scope.mediaPlayer.player.setVolume(volume); + } + }; + } + }) + + .directive('mySlider', function ($window) { + return { + link: link, + templateUrl: templateUrl('slider') + }; + + function link ($scope, element, attrs) { + var wrap = $('.tg_slider_wrap', element); + var fill = $('.tg_slider_track_fill', element); + var thumb = $('.tg_slider_thumb', element); + var width = wrap.width(); + var thumbWidth = Math.ceil(thumb.width()); + var model = attrs.sliderModel; + var sliderCallback = attrs.sliderOnchange; + var minValue = 0.0; + var maxValue = 1.0; + var lastUpdValue = false; + var lastMinPageX = false; + + if (attrs.sliderMin) { + $scope.$watch(attrs.sliderMin, function (newMinValue) { + minValue = newMinValue || 0.0; + }); + } + if (attrs.sliderMax) { + $scope.$watch(attrs.sliderMax, function (newMaxValue) { + maxValue = newMaxValue || 1.0; + }); + } + + var onMouseMove = function (e) { + var offsetX = e.pageX - lastMinPageX; + offsetX = Math.min(width, Math.max(0 , offsetX)); + // console.log('mmove', lastMinPageX, e.pageX, offsetX); + lastUpdValue = minValue + offsetX / width * (maxValue - minValue); + if (sliderCallback) { + $scope.$eval(sliderCallback, {value: lastUpdValue}); + } else { + $scope.$eval(model + '=' + lastUpdValue); + } + + thumb.css('left', Math.max(0, offsetX - thumbWidth)); + fill.css('width', offsetX); + + return cancelEvent(e); + }; + var stopMouseTrack = function () { + $($window).off('mousemove', onMouseMove); + $($window).off('mouseup', stopMouseTrack); + }; + + $scope.$watch(model, function (newVal) { + if (newVal != lastUpdValue && newVal !== undefined) { + var percent = Math.max(0, (newVal - minValue) / (maxValue - minValue)); + if (width) { + var offsetX = Math.ceil(width * percent); + offsetX = Math.min(width, Math.max(0 , offsetX)); + thumb.css('left', Math.max(0, offsetX - thumbWidth)); + fill.css('width', offsetX); + } else { + thumb.css('left', percent * 100 + '%'); + fill.css('width', percent * 100 + '%'); + } + lastUpdValue = false; + } + }); + + element.on('dragstart selectstart', cancelEvent); + + element.on('mousedown', function (e) { + if (!width) { + width = wrap.width(); + if (!width) { + console.error('empty width'); + return cancelEvent(e); + } + } + stopMouseTrack(); + + lastMinPageX = e.pageX - e.offsetX; + // console.log('mdown', lastMinPageX, e.pageX, e.offsetX); + lastUpdValue = minValue + e.offsetX / width * (maxValue - minValue); + if (sliderCallback) { + $scope.$eval(sliderCallback, {value: lastUpdValue}); + } else { + $scope.$eval(model + '=' + lastUpdValue); + } + + thumb.css('left', Math.max(0, e.offsetX - thumbWidth)); + fill.css('width', e.offsetX); + + $($window).on('mousemove', onMouseMove); + $($window).on('mouseup', stopMouseTrack); + + return cancelEvent(e); + }); } + }) diff --git a/app/js/filters.js b/app/js/filters.js index 0ac198c7..8666b6f8 100644 --- a/app/js/filters.js +++ b/app/js/filters.js @@ -56,7 +56,7 @@ angular.module('myApp.filters', ['myApp.i18n']) var cachedDates = {}, dateFilter = $filter('date'); - return function (timestamp) { + return function (timestamp, extended) { if (cachedDates[timestamp]) { return cachedDates[timestamp]; @@ -67,11 +67,12 @@ angular.module('myApp.filters', ['myApp.i18n']) format = 'shortTime'; if (diff > 518400000) { // 6 days - format = 'shortDate'; + format = extended ? 'mediumDate' : 'shortDate'; } else if (diff > 43200000) { // 12 hours - format = 'EEE'; + format = extended ? 'EEEE' : 'EEE'; } + return cachedDates[timestamp] = dateFilter(ticks, format); } }) @@ -120,6 +121,14 @@ angular.module('myApp.filters', ['myApp.i18n']) } }]) + .filter('durationRemains', function($filter) { + var durationFilter = $filter('duration'); + + return function (done, total) { + return '-' + durationFilter(total - done); + } + }) + .filter('phoneNumber', [function() { return function (phoneRaw) { var nbsp = ' '; @@ -140,13 +149,13 @@ angular.module('myApp.filters', ['myApp.i18n']) return size + ' b'; } else if (size < 1048576) { - return (Math.round(size / 1024 * 10) / 10) + ' Kb'; + return Math.round(size / 1024) + ' Kb'; } var mbs = size / 1048576; if (progressing) { mbs = mbs.toFixed(1); } else { - mbs = (Math.round(mbs * 100) / 100); + mbs = (Math.round(mbs * 10) / 10); } return mbs + ' Mb'; } @@ -192,14 +201,14 @@ angular.module('myApp.filters', ['myApp.i18n']) if (diff < 60000) { return _('relative_time_just_now'); } - if (diff < 3000000) { - var minutes = Math.ceil(diff / 60000); + if (diff < 3600000) { + var minutes = Math.floor(diff / 60000); return langMinutesPluralize(minutes); } - if (diff < 10000000) { - var hours = Math.ceil(diff / 3600000); + if (diff < 86400000) { + var hours = Math.floor(diff / 3600000); return langHoursPluralize(hours); } - return dateOrTimeFilter(timestamp); + return dateOrTimeFilter(timestamp, true); } }) diff --git a/app/js/i18n.js b/app/js/i18n.js index 4e02f554..197fc73a 100644 --- a/app/js/i18n.js +++ b/app/js/i18n.js @@ -21,16 +21,6 @@ angular.module('myApp.i18n', ['izhukov.utils']) }); } - function encodeEntities(value) { - return value. - replace(/&/g, '&'). - replace(/([^\#-~| |!\n\*])/g, function (value) { // non-alphanumeric - return '&#' + value.charCodeAt(0) + ';'; - }). - replace(//g, '>'); - } - function parseMarkdownString(msgstr, msgid) { msgstr = msgstr.replace(/\*\*(.+?)\*\*/g, "$1") .replace(/\n/g, "
"); diff --git a/app/js/lib/bin_utils.js b/app/js/lib/bin_utils.js index 38bbf77f..d59b2abe 100644 --- a/app/js/lib/bin_utils.js +++ b/app/js/lib/bin_utils.js @@ -112,11 +112,13 @@ function bytesXor (bytes1, bytes2) { } function bytesToWords (bytes) { + if (bytes instanceof ArrayBuffer) { + bytes = new Uint8Array(bytes); + } var len = bytes.length, - words = []; - - for (var i = 0; i < len; i++) { - words[i >>> 2] |= bytes[i] << (24 - (i % 4) * 8); + words = [], i; + for (i = 0; i < len; i++) { + words[i >>> 2] |= bytes[i] << (24 - (i % 4) * 8); } return new CryptoJS.lib.WordArray.init(words, len); @@ -128,7 +130,7 @@ function bytesFromWords (wordArray) { bytes = []; for (var i = 0; i < sigBytes; i++) { - bytes.push((words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff); + bytes.push((words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff); } return bytes; @@ -154,18 +156,59 @@ function bytesToArrayBuffer (b) { return (new Uint8Array(b)).buffer; } +function convertToArrayBuffer(bytes) { + // Be careful with converting subarrays!! + if (bytes instanceof ArrayBuffer) { + return bytes; + } + if (bytes.buffer !== undefined && + bytes.buffer.byteLength == bytes.length * bytes.BYTES_PER_ELEMENT) { + return bytes.buffer; + } + return bytesToArrayBuffer(bytes); +} + +function convertToUint8Array(bytes) { + if (bytes.buffer !== undefined) { + return bytes; + } + return new Uint8Array(bytes); +} + +function convertToByteArray(bytes) { + if (Array.isArray(bytes)) { + return bytes; + } + bytes = convertToUint8Array(bytes); + var newBytes = []; + for (var i = 0, len = bytes.length; i < len; i++) { + newBytes.push(bytes[i]); + } + return newBytes; +} + function bytesFromArrayBuffer (buffer) { var len = buffer.byteLength, byteView = new Uint8Array(buffer), bytes = []; for (var i = 0; i < len; ++i) { - bytes[i] = byteView[i]; + bytes[i] = byteView[i]; } return bytes; } +function bufferConcat(buffer1, buffer2) { + var l1 = buffer1.byteLength || buffer1.length, + l2 = buffer2.byteLength || buffer2.length; + var tmp = new Uint8Array(l1 + l2); + tmp.set(buffer1 instanceof ArrayBuffer ? new Uint8Array(buffer1) : buffer1, 0); + tmp.set(buffer2 instanceof ArrayBuffer ? new Uint8Array(buffer2) : buffer2, l1); + + return tmp.buffer; +} + function longToInts (sLong) { var divRem = bigStringInt(sLong).divideAndRemainder(bigint(0x100000000)); @@ -195,24 +238,24 @@ function uintToInt (val) { return val; } -function sha1Hash (bytes) { - // console.log('SHA-1 hash start'); - var hashBytes = sha1.hash(bytes, true); - // console.log('SHA-1 hash finish'); +function sha1HashSync (bytes) { + this.rushaInstance = this.rushaInstance || new Rusha(1024 * 1024); + + // console.log(dT(), 'SHA-1 hash start', bytes.byteLength || bytes.length); + var hashBytes = rushaInstance.rawDigest(bytes).buffer; + // console.log(dT(), 'SHA-1 hash finish'); return hashBytes; } +function sha1BytesSync (bytes) { + return bytesFromArrayBuffer(sha1HashSync(bytes)); +} -function rsaEncrypt (publicKey, bytes) { - var needPadding = 255 - bytes.length; - if (needPadding > 0) { - var padding = new Array(needPadding); - (new SecureRandom()).nextBytes(padding); - bytes = bytes.concat(padding); - } +function rsaEncrypt (publicKey, bytes) { + bytes = addPadding(bytes, 255); // console.log('RSA encrypt start'); var N = new BigInteger(publicKey.modulus, 16), @@ -220,23 +263,35 @@ function rsaEncrypt (publicKey, bytes) { X = new BigInteger(bytes), encryptedBigInt = X.modPowInt(E, N), encryptedBytes = bytesFromBigInt(encryptedBigInt, 256); - // console.log('RSA encrypt finish'); return encryptedBytes; } -function aesEncrypt (bytes, keyBytes, ivBytes) { - // console.log('AES encrypt start', bytes.length/*, bytesToHex(keyBytes), bytesToHex(ivBytes)*/); - - var needPadding = 16 - (bytes.length % 16); - if (needPadding > 0 && needPadding < 16) { +function addPadding(bytes, blockSize) { + blockSize = blockSize || 16; + var len = bytes.byteLength || bytes.length; + var needPadding = blockSize - (len % blockSize); + if (needPadding > 0 && needPadding < blockSize) { var padding = new Array(needPadding); (new SecureRandom()).nextBytes(padding); - bytes = bytes.concat(padding); + if (bytes instanceof ArrayBuffer) { + bytes = bufferConcat(bytes, padding); + } else { + bytes = bytes.concat(padding); + } } + return bytes; +} + +function aesEncryptSync (bytes, keyBytes, ivBytes) { + var len = bytes.byteLength || bytes.length; + + // console.log(dT(), 'AES encrypt start', len/*, bytesToHex(keyBytes), bytesToHex(ivBytes)*/); + bytes = addPadding(bytes); + var encryptedWords = CryptoJS.AES.encrypt(bytesToWords(bytes), bytesToWords(keyBytes), { iv: bytesToWords(ivBytes), padding: CryptoJS.pad.NoPadding, @@ -244,15 +299,14 @@ function aesEncrypt (bytes, keyBytes, ivBytes) { }).ciphertext; var encryptedBytes = bytesFromWords(encryptedWords); - - // console.log('AES encrypt finish'); + // console.log(dT(), 'AES encrypt finish'); return encryptedBytes; } -function aesDecrypt (encryptedBytes, keyBytes, ivBytes) { - // console.log('AES decrypt start', encryptedBytes.length/*, bytesToHex(keyBytes), bytesToHex(ivBytes)*/); +function aesDecryptSync (encryptedBytes, keyBytes, ivBytes) { + // console.log(dT(), 'AES decrypt start', encryptedBytes.length); var decryptedWords = CryptoJS.AES.decrypt({ciphertext: bytesToWords(encryptedBytes)}, bytesToWords(keyBytes), { iv: bytesToWords(ivBytes), padding: CryptoJS.pad.NoPadding, @@ -260,8 +314,7 @@ function aesDecrypt (encryptedBytes, keyBytes, ivBytes) { }); var bytes = bytesFromWords(decryptedWords); - - // console.log('AES decrypt finish'); + // console.log(dT(), 'AES decrypt finish'); return bytes; } @@ -281,7 +334,7 @@ function pqPrimeFactorization (pqBytes) { var what = new BigInteger(pqBytes), result = false; - console.log('PQ start', pqBytes, what.toString(16), what.bitLength()); + // console.log(dT(), 'PQ start', pqBytes, what.toString(16), what.bitLength()); try { result = pqPrimeLeemon(str2bigInt(what.toString(16), 16, Math.ceil(64 / bpe) + 1)) @@ -306,7 +359,7 @@ function pqPrimeFactorization (pqBytes) { // console.timeEnd('pq BigInt'); } - console.log('PQ finish'); + // console.log(dT(), 'PQ finish'); return result; } @@ -520,13 +573,15 @@ function pqPrimeLeemon (what) { function bytesModPow (x, y, m) { try { - var xBigInt = str2bigInt(x, 64), - yBigInt = str2bigInt(y, 64), + var xBigInt = str2bigInt(bytesToHex(x), 16), + yBigInt = str2bigInt(bytesToHex(y), 16), mBigInt = str2bigInt(bytesToHex(m), 16, 2), resBigInt = powMod(xBigInt, yBigInt, mBigInt); return bytesFromHex(bigInt2str(resBigInt, 16)); - } catch (e) {} + } catch (e) { + console.error('mod pow error', e); + } return bytesFromBigInt(new BigInteger(x).modPow(new BigInteger(y), new BigInteger(m))); } diff --git a/app/js/lib/config.js b/app/js/lib/config.js index 93bb1001..80f2aa85 100644 --- a/app/js/lib/config.js +++ b/app/js/lib/config.js @@ -27,6 +27,7 @@ Config.App = { Config.Modes = { test: location.search.indexOf('test=1') > 0, debug: location.search.indexOf('debug=1') > 0, + ssl: location.search.indexOf('ssl=1') > 0 || location.protocol == 'https:', packed: location.protocol == 'app:' || location.protocol == 'chrome-extension:', ios_standalone: window.navigator.standalone && navigator.userAgent.match(/iOS|iPhone|iPad/), chrome_packed: window.chrome && chrome.app && chrome.app.window && true || false @@ -132,7 +133,7 @@ Config.LangCountries = {"es": "ES", "ru": "RU", "en": "US", "de": "DE", "it": "I for (i = 0; i < keys.length; i++) { key = keys[i] = prefix + keys[i]; - if (cache[key] !== undefined) { + if (key.substr(0, 3) != 'xt_' && cache[key] !== undefined) { result.push(cache[key]); } else if (useLs) { diff --git a/app/js/lib/crypto_worker.js b/app/js/lib/crypto_worker.js index 3340a9d6..96ec3d30 100644 --- a/app/js/lib/crypto_worker.js +++ b/app/js/lib/crypto_worker.js @@ -11,7 +11,8 @@ importScripts( '../../vendor/jsbn/jsbn_combined.js', '../../vendor/leemon_bigint/bigint.js', '../../vendor/closure/long.js', - '../../vendor/cryptoJS/crypto.js' + '../../vendor/cryptoJS/crypto.js', + '../../vendor/rusha/rusha.js' ); onmessage = function (e) { @@ -28,15 +29,15 @@ onmessage = function (e) { break; case 'sha1-hash': - result = sha1Hash(e.data.bytes); + result = sha1HashSync(e.data.bytes); break; case 'aes-encrypt': - result = aesEncrypt(e.data.bytes, e.data.keyBytes, e.data.ivBytes); + result = aesEncryptSync(e.data.bytes, e.data.keyBytes, e.data.ivBytes); break; case 'aes-decrypt': - result = aesDecrypt(e.data.encryptedBytes, e.data.keyBytes, e.data.ivBytes); + result = aesDecryptSync(e.data.encryptedBytes, e.data.keyBytes, e.data.ivBytes); break; default: @@ -45,3 +46,5 @@ onmessage = function (e) { postMessage({taskID: taskID, result: result}); } + +postMessage('ready'); diff --git a/app/js/lib/mtproto.js b/app/js/lib/mtproto.js index 880e32f3..389b985f 100644 --- a/app/js/lib/mtproto.js +++ b/app/js/lib/mtproto.js @@ -8,6 +8,8 @@ angular.module('izhukov.mtproto', ['izhukov.utils']) .factory('MtpDcConfigurator', function () { + var sslSubdomains = ['pluto', 'venus', 'aurora', 'vesta', 'flora']; + var dcOptions = Config.Modes.test ? [ {id: 1, host: '173.240.5.253', port: 80}, @@ -24,14 +26,23 @@ angular.module('izhukov.mtproto', ['izhukov.utils']) var chosenServers = {}; - function chooseServer(dcID) { + function chooseServer(dcID, upload) { if (chosenServers[dcID] === undefined) { var chosenServer = false, i, dcOption; + + if (Config.Modes.ssl) { + var subdomain = sslSubdomains[dcID - 1] + (upload ? '-1' : ''); + var path = Config.Modes.test ? 'apiw_test1' : 'apiw1'; + chosenServer = 'https://' + subdomain + '.web.telegram.org/' + path; + return chosenServer; + } + for (i = 0; i < dcOptions.length; i++) { dcOption = dcOptions[i]; if (dcOption.id == dcID) { - chosenServer = dcOption.host + ':' + dcOption.port; + chosenServer = 'http://' + dcOption.host + (dcOption.port != 80 ? ':' + dcOption.port : '') + '/apiw1'; + break; } } chosenServers[dcID] = chosenServer; @@ -82,7 +93,7 @@ angular.module('izhukov.mtproto', ['izhukov.utils']) var buffer = RSAPublicKey.getBuffer(); - var fingerprintBytes = sha1Hash(buffer).slice(-8); + var fingerprintBytes = sha1BytesSync(buffer).slice(-8); fingerprintBytes.reverse(); publicKeysParsed[bytesToHex(fingerprintBytes)] = { @@ -114,7 +125,8 @@ angular.module('izhukov.mtproto', ['izhukov.utils']) }; }) -.service('MtpSecureRandom', function () { +.service('MtpSecureRandom', function ($window) { + $($window).on('click keydown', rng_seed_time); return new SecureRandom(); }) @@ -143,7 +155,7 @@ angular.module('izhukov.mtproto', ['izhukov.utils']) lastMessageID = messageID; - // console.log('generated msg id', messageID); + // console.log('generated msg id', messageID, timeOffset); return longFromInts(messageID[0], messageID[1]); }; @@ -169,7 +181,11 @@ angular.module('izhukov.mtproto', ['izhukov.utils']) .factory('MtpAuthorizer', function (MtpDcConfigurator, MtpRsaKeysManager, MtpSecureRandom, MtpTimeManager, CryptoWorker, $http, $q, $timeout) { var chromeMatches = navigator.userAgent.match(/Chrome\/(\d+(\.\d+)?)/), - chromeVersion = chromeMatches && parseFloat(chromeMatches[1]) || false; + chromeVersion = chromeMatches && parseFloat(chromeMatches[1]) || false, + xhrSendBuffer = !('ArrayBufferView' in window) && (!chromeVersion || chromeVersion < 30); + + delete $http.defaults.headers.post['Content-Type']; + delete $http.defaults.headers.common['Accept']; function mtpSendPlainRequest (dcID, requestBuffer) { var requestLength = requestBuffer.byteLength, @@ -190,16 +206,10 @@ angular.module('izhukov.mtproto', ['izhukov.utils']) resultArray.set(headerArray); resultArray.set(requestArray, headerArray.length); - delete $http.defaults.headers.post['Content-Type']; - delete $http.defaults.headers.common['Accept']; - - if (!('ArrayBufferView' in window) && (!chromeVersion || chromeVersion < 30)) { - resultArray = resultArray.buffer; - } - - var requestPromise; + var requestData = xhrSendBuffer ? resultBuffer : resultArray, + requestPromise; try { - requestPromise = $http.post('http://' + MtpDcConfigurator.chooseServer(dcID) + '/apiw1', resultArray, { + requestPromise = $http.post(MtpDcConfigurator.chooseServer(dcID), requestData, { responseType: 'arraybuffer', transformRequest: null }); @@ -223,8 +233,6 @@ angular.module('izhukov.mtproto', ['izhukov.utils']) return $q.reject({code: 406, type: 'NETWORK_BAD_RESPONSE', originalError: e}); } - rng_seed_time(); - return deserializer; }, function (error) { @@ -305,7 +313,7 @@ angular.module('izhukov.mtproto', ['izhukov.utils']) new_nonce: auth.newNonce }, 'P_Q_inner_data', 'DECRYPTED_DATA'); - var dataWithHash = sha1Hash(data.getBuffer()).concat(data.getBytes()); + var dataWithHash = sha1BytesSync(data.getBuffer()).concat(data.getBytes()); var request = new TLSerialization({mtproto: true}); request.storeMethod('req_DH_params', { @@ -337,7 +345,7 @@ angular.module('izhukov.mtproto', ['izhukov.utils']) } if (response._ == 'server_DH_params_fail') { - var newNonceHash = sha1Hash(auth.newNonce).slice(-16) + var newNonceHash = sha1BytesSync(auth.newNonce).slice(-16); if (!bytesCmp (newNonceHash, response.new_nonce_hash)) { deferred.reject(new Error('server_DH_params_fail new_nonce_hash mismatch')); return false; @@ -362,10 +370,10 @@ angular.module('izhukov.mtproto', ['izhukov.utils']) function mtpDecryptServerDhDataAnswer (auth, encryptedAnswer) { auth.localTime = tsNow(); - auth.tmpAesKey = sha1Hash(auth.newNonce.concat(auth.serverNonce)).concat(sha1Hash(auth.serverNonce.concat(auth.newNonce)).slice(0, 12)); - auth.tmpAesIv = sha1Hash(auth.serverNonce.concat(auth.newNonce)).slice(12).concat(sha1Hash([].concat(auth.newNonce, auth.newNonce)), auth.newNonce.slice(0, 4)); + auth.tmpAesKey = sha1BytesSync(auth.newNonce.concat(auth.serverNonce)).concat(sha1BytesSync(auth.serverNonce.concat(auth.newNonce)).slice(0, 12)); + auth.tmpAesIv = sha1BytesSync(auth.serverNonce.concat(auth.newNonce)).slice(12).concat(sha1BytesSync([].concat(auth.newNonce, auth.newNonce)), auth.newNonce.slice(0, 4)); - var answerWithHash = aesDecrypt(encryptedAnswer, auth.tmpAesKey, auth.tmpAesIv); + var answerWithHash = aesDecryptSync(encryptedAnswer, auth.tmpAesKey, auth.tmpAesIv); var hash = answerWithHash.slice(0, 20); var answerWithPadding = answerWithHash.slice(20); @@ -395,7 +403,7 @@ angular.module('izhukov.mtproto', ['izhukov.utils']) var offset = deserializer.getOffset(); - if (!bytesCmp(hash, sha1Hash(answerWithPadding.slice(0, offset)))) { + if (!bytesCmp(hash, sha1BytesSync(answerWithPadding.slice(0, offset)))) { throw new Error('server_DH_inner_data SHA1-hash mismatch'); } @@ -420,9 +428,9 @@ angular.module('izhukov.mtproto', ['izhukov.utils']) g_b: gB, }, 'Client_DH_Inner_Data'); - var dataWithHash = sha1Hash(data.getBuffer()).concat(data.getBytes()); + var dataWithHash = sha1BytesSync(data.getBuffer()).concat(data.getBytes()); - var encryptedData = aesEncrypt(dataWithHash, auth.tmpAesKey, auth.tmpAesIv); + var encryptedData = aesEncryptSync(dataWithHash, auth.tmpAesKey, auth.tmpAesIv); var request = new TLSerialization({mtproto: true}); request.storeMethod('set_client_DH_params', { @@ -451,14 +459,14 @@ angular.module('izhukov.mtproto', ['izhukov.utils']) } CryptoWorker.modPow(auth.gA, auth.b, auth.dhPrime).then(function (authKey) { - var authKeyHash = sha1Hash(authKey), + var authKeyHash = sha1BytesSync(authKey), authKeyAux = authKeyHash.slice(0, 8), authKeyID = authKeyHash.slice(-8); console.log(dT(), 'Got Set_client_DH_params_answer', response._); switch (response._) { case 'dh_gen_ok': - var newNonceHash1 = sha1Hash(auth.newNonce.concat([1], authKeyAux)).slice(-16); + var newNonceHash1 = sha1BytesSync(auth.newNonce.concat([1], authKeyAux)).slice(-16); if (!bytesCmp(newNonceHash1, response.new_nonce_hash1)) { deferred.reject(new Error('Set_client_DH_params_answer new_nonce_hash1 mismatch')); @@ -476,7 +484,7 @@ angular.module('izhukov.mtproto', ['izhukov.utils']) break; case 'dh_gen_retry': - var newNonceHash2 = sha1Hash(auth.newNonce.concat([2], authKeyAux)).slice(-16); + var newNonceHash2 = sha1BytesSync(auth.newNonce.concat([2], authKeyAux)).slice(-16); if (!bytesCmp(newNonceHash2, response.new_nonce_hash2)) { deferred.reject(new Error('Set_client_DH_params_answer new_nonce_hash2 mismatch')); return false; @@ -485,7 +493,7 @@ angular.module('izhukov.mtproto', ['izhukov.utils']) return mtpSendSetClientDhParams(auth); case 'dh_gen_fail': - var newNonceHash3 = sha1Hash(auth.newNonce.concat([3], authKeyAux)).slice(-16); + var newNonceHash3 = sha1BytesSync(auth.newNonce.concat([3], authKeyAux)).slice(-16); if (!bytesCmp(newNonceHash3, response.new_nonce_hash3)) { deferred.reject(new Error('Set_client_DH_params_answer new_nonce_hash3 mismatch')); return false; @@ -552,8 +560,13 @@ angular.module('izhukov.mtproto', ['izhukov.utils']) iii = 0, offline, offlineInited = false, + akStopped = false, chromeMatches = navigator.userAgent.match(/Chrome\/(\d+(\.\d+)?)/), - chromeVersion = chromeMatches && parseFloat(chromeMatches[1]) || false; + chromeVersion = chromeMatches && parseFloat(chromeMatches[1]) || false, + xhrSendBuffer = !('ArrayBufferView' in window) && (!chromeVersion || chromeVersion < 30); + + delete $http.defaults.headers.post['Content-Type']; + delete $http.defaults.headers.common['Accept']; $rootScope.retryOnline = function () { $(document.body).trigger('online'); @@ -566,7 +579,9 @@ angular.module('izhukov.mtproto', ['izhukov.utils']) this.iii = iii++; this.authKey = authKey; - this.authKeyID = sha1Hash(authKey).slice(-8); + this.authKeyUint8 = convertToUint8Array(authKey); + this.authKeyBuffer = convertToArrayBuffer(authKey); + this.authKeyID = sha1BytesSync(authKey).slice(-8); this.serverSalt = serverSalt; @@ -738,7 +753,7 @@ angular.module('izhukov.mtproto', ['izhukov.utils']) message = { msg_id: messageID, seq_no: seqNo, - body: serializer.getBytes(), + body: serializer.getBytes(true), isAPI: true }; @@ -754,7 +769,9 @@ angular.module('izhukov.mtproto', ['izhukov.utils']) MtpNetworker.prototype.checkLongPoll = function(force) { var isClean = this.cleanupSent(); // console.log('Check lp', this.longPollPending, tsNow(), this.dcID, isClean); - if (this.longPollPending && tsNow() < this.longPollPending || this.offline) { + if (this.longPollPending && tsNow() < this.longPollPending || + this.offline || + akStopped) { return false; } var self = this; @@ -787,7 +804,7 @@ angular.module('izhukov.mtproto', ['izhukov.utils']) longPoll: true }).then(function () { delete self.longPollPending; - $timeout(self.checkLongPoll.bind(self), 0); + setZeroTimeout(self.checkLongPoll.bind(self)); }, function () { console.log('Long-poll failed'); }); @@ -827,19 +844,47 @@ angular.module('izhukov.mtproto', ['izhukov.utils']) }; MtpNetworker.prototype.getMsgKeyIv = function (msgKey, isOut) { - var authKey = this.authKey, - x = isOut ? 0 : 8; - - var promises = { - sha1a: CryptoWorker.sha1Hash(msgKey.concat(authKey.slice(x, x + 32))), - sha1b: CryptoWorker.sha1Hash(authKey.slice(32 + x, 48 + x).concat(msgKey, authKey.slice(48 + x, 64 + x))), - sha1c: CryptoWorker.sha1Hash(authKey.slice(64 + x, 96 + x).concat(msgKey)), - sha1d: CryptoWorker.sha1Hash(msgKey.concat(authKey.slice(96 + x, 128 + x))) - }; + var authKey = this.authKeyUint8, + x = isOut ? 0 : 8, + sha1aText = new Uint8Array(48), + sha1bText = new Uint8Array(48), + sha1cText = new Uint8Array(48), + sha1dText = new Uint8Array(48), + promises = {}; + + sha1aText.set(msgKey, 0); + sha1aText.set(authKey.subarray(x, x + 32), 16); + promises.sha1a = CryptoWorker.sha1Hash(sha1aText); + + sha1bText.set(authKey.subarray(x + 32, x + 48), 0); + sha1bText.set(msgKey, 16); + sha1bText.set(authKey.subarray(x + 48, x + 64), 32); + promises.sha1b = CryptoWorker.sha1Hash(sha1bText); + + sha1cText.set(authKey.subarray(x + 64, x + 96), 0); + sha1cText.set(msgKey, 32); + promises.sha1c = CryptoWorker.sha1Hash(sha1cText); + + sha1dText.set(msgKey, 0); + sha1dText.set(authKey.subarray(x + 96, x + 128), 16); + promises.sha1d = CryptoWorker.sha1Hash(sha1dText); return $q.all(promises).then(function (result) { - var aesKey = result.sha1a.slice(0, 8).concat(result.sha1b.slice(8, 20), result.sha1c.slice(4, 16)); - var aesIv = result.sha1a.slice(8, 20).concat(result.sha1b.slice(0, 8), result.sha1c.slice(16, 20), result.sha1d.slice(0, 8)); + var aesKey = new Uint8Array(32), + aesIv = new Uint8Array(32); + sha1a = new Uint8Array(result.sha1a), + sha1b = new Uint8Array(result.sha1b), + sha1c = new Uint8Array(result.sha1c), + sha1d = new Uint8Array(result.sha1d); + + aesKey.set(sha1a.subarray(0, 8)); + aesKey.set(sha1b.subarray(8, 20), 8); + aesKey.set(sha1c.subarray(4, 16), 20); + + aesIv.set(sha1a.subarray(8, 20)); + aesIv.set(sha1b.subarray(0, 8), 12); + aesIv.set(sha1c.subarray(16, 20), 20); + aesIv.set(sha1d.subarray(0, 8), 24); return [aesKey, aesIv]; }); @@ -916,8 +961,8 @@ angular.module('izhukov.mtproto', ['izhukov.utils']) MtpNetworker.prototype.performSheduledRequest = function() { - // console.trace('sheduled', this.dcID, this.iii); - if (this.offline) { + // console.log(dT(), 'sheduled', this.dcID, this.iii); + if (this.offline || akStopped) { console.log(dT(), 'Cancel sheduled'); return false; } @@ -948,13 +993,32 @@ angular.module('izhukov.mtproto', ['izhukov.utils']) currentTime = tsNow(), hasApiCall = false, hasHttpWait = false, + lengthOverflow = false, + singlesCount = 0, self = this; angular.forEach(this.pendingMessages, function (value, messageID) { if (!value || value >= currentTime) { if (message = self.sentMessages[messageID]) { + var messageByteLength = (message.body.byteLength || message.body.length) + 32; + if (!message.notContentRelated && + lengthOverflow) { + return; + } + if (!message.notContentRelated && + messagesByteLen && + messagesByteLen + messageByteLength > 655360) { // 640 Kb + lengthOverflow = true; + return; + } + if (message.singleInRequest) { + singlesCount++; + if (singlesCount > 1) { + return; + } + } messages.push(message); - messagesByteLen += message.body.length + 32; + messagesByteLen += messageByteLength; if (message.isAPI) { hasApiCall = true; } @@ -1009,7 +1073,7 @@ angular.module('izhukov.mtproto', ['izhukov.utils']) inner: innerMessages } - message = angular.extend({body: container.getBytes()}, containerSentMessage); + message = angular.extend({body: container.getBytes(true)}, containerSentMessage); this.sentMessages[message.msg_id] = containerSentMessage; @@ -1071,15 +1135,23 @@ angular.module('izhukov.mtproto', ['izhukov.utils']) self.toggleOffline(true); }); + + if (lengthOverflow || singlesCount > 1) { + this.sheduleRequest() + } }; MtpNetworker.prototype.getEncryptedMessage = function (bytes) { var self = this; + // console.log(dT(), 'Start encrypt', bytes.byteLength); return CryptoWorker.sha1Hash(bytes).then(function (bytesHash) { - var msgKey = bytesHash.slice(-16); + // console.log(dT(), 'after hash'); + var msgKey = new Uint8Array(bytesHash).subarray(4, 20); return self.getMsgKeyIv(msgKey, true).then(function (keyIv) { + // console.log(dT(), 'after msg key iv'); return CryptoWorker.aesEncrypt(bytes, keyIv[0], keyIv[1]).then(function (encryptedBytes) { + // console.log(dT(), 'Finish encrypt'); return { bytes: encryptedBytes, msgKey: msgKey @@ -1090,7 +1162,9 @@ angular.module('izhukov.mtproto', ['izhukov.utils']) }; MtpNetworker.prototype.getDecryptedMessage = function (msgKey, encryptedData) { + // console.log(dT(), 'get decrypted start'); return this.getMsgKeyIv(msgKey, false).then(function (keyIv) { + // console.log(dT(), 'after msg key iv'); return CryptoWorker.aesDecrypt(encryptedData, keyIv[0], keyIv[1]); }); }; @@ -1111,20 +1185,14 @@ angular.module('izhukov.mtproto', ['izhukov.utils']) data.storeInt(message.body.length, 'message_data_length'); data.storeRawBytes(message.body, 'message_data'); - return this.getEncryptedMessage(data.getBytes()).then(function (encryptedResult) { + return this.getEncryptedMessage(data.getBuffer()).then(function (encryptedResult) { // console.log(dT(), 'Got encrypted out message'/*, encryptedResult*/); - var request = new TLSerialization({startMaxLength: encryptedResult.bytes.length + 256}); + var request = new TLSerialization({startMaxLength: encryptedResult.bytes.byteLength + 256}); request.storeIntBytes(self.authKeyID, 64, 'auth_key_id'); request.storeIntBytes(encryptedResult.msgKey, 128, 'msg_key'); request.storeRawBytes(encryptedResult.bytes, 'encrypted_data'); - delete $http.defaults.headers.post['Content-Type']; - delete $http.defaults.headers.common['Accept']; - - var resultArray = request.getArray(); - if (!('ArrayBufferView' in window) && (!chromeVersion || chromeVersion < 30)) { - resultArray = resultArray.buffer; - } + var requestData = xhrSendBuffer ? request.getBuffer() : request.getArray(); var requestPromise; try { @@ -1132,7 +1200,7 @@ angular.module('izhukov.mtproto', ['izhukov.utils']) responseType: 'arraybuffer', transformRequest: null }); - requestPromise = $http.post('http://' + MtpDcConfigurator.chooseServer(self.dcID) + '/apiw1', resultArray, options); + requestPromise = $http.post(MtpDcConfigurator.chooseServer(self.dcID, self.upload), requestData, options); } catch (e) { requestPromise = $q.reject(e); } @@ -1168,32 +1236,31 @@ angular.module('izhukov.mtproto', ['izhukov.utils']) var deserializer = new TLDeserialization(responseBuffer); - var authKeyID = deserializer.fetchIntBytes(64, 'auth_key_id'); + var authKeyID = deserializer.fetchIntBytes(64, false, 'auth_key_id'); if (!bytesCmp(authKeyID, this.authKeyID)) { throw new Error('Invalid server auth_key_id: ' + bytesToHex(authKeyID)); } - var msgKey = deserializer.fetchIntBytes(128, 'msg_key'); - - var dataLength = responseBuffer.byteLength - deserializer.getOffset(); - var encryptedData = deserializer.fetchRawBytes(dataLength, 'encrypted_data'); + var msgKey = deserializer.fetchIntBytes(128, true, 'msg_key'), + encryptedData = deserializer.fetchRawBytes(responseBuffer.byteLength - deserializer.getOffset(), true, 'encrypted_data'); return this.getDecryptedMessage(msgKey, encryptedData).then(function (dataWithPadding) { - var buffer = bytesToArrayBuffer(dataWithPadding); - - var deserializer = new TLDeserialization(buffer, {mtproto: true}); + // console.log(dT(), 'after decrypt'); + var deserializer = new TLDeserialization(dataWithPadding, {mtproto: true}); - var salt = deserializer.fetchIntBytes(64, 'salt'); - var sessionID = deserializer.fetchIntBytes(64, 'session_id'); + var salt = deserializer.fetchIntBytes(64, false, 'salt'); + var sessionID = deserializer.fetchIntBytes(64, false, 'session_id'); var messageID = deserializer.fetchLong('message_id'); var seqNo = deserializer.fetchInt('seq_no'); - var messageBody = deserializer.fetchRawBytes(false, 'message_data'); + var messageBody = deserializer.fetchRawBytes(false, true, 'message_data'); - var offset = deserializer.getOffset(); + // console.log(dT(), 'before hash'); + var hashData = convertToUint8Array(dataWithPadding).subarray(0, deserializer.getOffset()); - return CryptoWorker.sha1Hash(dataWithPadding.slice(0, offset)).then(function (dataHashed) { - if (!bytesCmp(msgKey, dataHashed.slice(-16))) { + return CryptoWorker.sha1Hash(hashData).then(function (dataHash) { + if (!bytesCmp(msgKey, bytesFromArrayBuffer(dataHash).slice(-16))) { + console.warn(msgKey, bytesFromArrayBuffer(dataHash)); throw new Error('server msgKey mismatch'); } @@ -1216,7 +1283,7 @@ angular.module('izhukov.mtproto', ['izhukov.utils']) } if (this.offset != offset + result.bytes) { console.warn(dT(), 'set offset', this.offset, offset, result.bytes); - console.log(dT(), result); + // console.log(dT(), result); this.offset = offset + result.bytes; } // console.log(dT(), 'override message', result); @@ -1233,7 +1300,6 @@ angular.module('izhukov.mtproto', ['izhukov.utils']) } }; var deserializer = new TLDeserialization(buffer, deserializerOptions); - var response = deserializer.fetchObject('', 'INPUT'); return { @@ -1267,19 +1333,19 @@ angular.module('izhukov.mtproto', ['izhukov.utils']) return false; } - // console.log('shedule req', delay); + // console.log(dT(), 'shedule req', delay); // console.trace(); $timeout.cancel(this.nextReqPromise); + if (delay > 0) { + this.nextReqPromise = $timeout(this.performSheduledRequest.bind(this), delay || 0); + } else { + setZeroTimeout(this.performSheduledRequest.bind(this)) + } - this.nextReqPromise = $timeout(this.performSheduledRequest.bind(this), delay || 0); this.nextReq = nextReq; }; - MtpNetworker.prototype.onSessionCreate = function (sessionID, messageID) { - // console.log(dT(), 'New session created', bytesToHex(sessionID)); - }; - MtpNetworker.prototype.ackMessage = function (msgID) { // console.log('ack message', msgID); this.pendingAcks.push(msgID); @@ -1400,7 +1466,13 @@ angular.module('izhukov.mtproto', ['izhukov.utils']) this.processMessageAck(message.first_msg_id); this.applyServerSalt(message.server_salt); - this.onSessionCreate(sessionID, messageID); + + var self = this; + Storage.get('dc').then(function (baseDcID) { + if (baseDcID == self.dcID && !self.upload && updatesProcessor) { + updatesProcessor(message); + } + }); break; case 'msgs_ack': @@ -1486,13 +1558,26 @@ angular.module('izhukov.mtproto', ['izhukov.utils']) } }; + function startAll() { + if (akStopped) { + akStopped = false; + updatesProcessor({_: 'new_session_created'}); + } + } + + function stopAll() { + akStopped = true; + } + return { getNetworker: function (dcID, authKey, serverSalt, options) { return new MtpNetworker(dcID, authKey, serverSalt, options); }, setUpdatesProcessor: function (callback) { updatesProcessor = callback; - } + }, + stopAll: stopAll, + startAll: startAll }; }) diff --git a/app/js/lib/mtproto_wrapper.js b/app/js/lib/mtproto_wrapper.js index 2315b8dd..e7363014 100644 --- a/app/js/lib/mtproto_wrapper.js +++ b/app/js/lib/mtproto_wrapper.js @@ -7,12 +7,14 @@ angular.module('izhukov.mtproto.wrapper', ['izhukov.utils', 'izhukov.mtproto']) -.factory('MtpApiManager', function (Storage, MtpAuthorizer, MtpNetworkerFactory, ErrorService, $q) { +.factory('MtpApiManager', function (Storage, MtpAuthorizer, MtpNetworkerFactory, MtpSingleInstanceService, ErrorService, $q) { var cachedNetworkers = {}, cachedUploadNetworkers = {}, cachedExportPromise = {}, baseDcID = false; + MtpSingleInstanceService.start(); + Storage.get('dc').then(function (dcID) { if (dcID) { baseDcID = dcID; @@ -54,7 +56,9 @@ angular.module('izhukov.mtproto.wrapper', ['izhukov.utils', 'izhukov.mtproto']) } if (cache[dcID] !== undefined) { - return $q.when(cache[dcID]); + return {then: function (cb) { + cb(cache[dcID]); + }}; } var akk = 'dc' + dcID + '_auth_key', @@ -120,24 +124,17 @@ angular.module('izhukov.mtproto.wrapper', ['izhukov.utils', 'izhukov.mtproto']) dcID, networkerPromise; - if (dcID = options.dcID) { - networkerPromise = mtpGetNetworker(dcID, options); - } else { - networkerPromise = Storage.get('dc').then(function (baseDcID) { - return mtpGetNetworker(dcID = baseDcID || 2, options); - }); + var cachedNetworker; + var stack = (new Error()).stack; + if (!stack) { + try {window.unexistingFunction();} catch (e) { + stack = e.stack || ''; + } } - - var cachedNetworker, - stack = false; - - networkerPromise.then(function (networker) { + var performRequest = function (networker) { return (cachedNetworker = networker).wrapApiCall(method, params, options).then( function (result) { deferred.resolve(result); - // $timeout(function () { - // deferred.resolve(result); - // }, 1000); }, function (error) { console.error(dT(), 'Error', error.code, error.type, baseDcID, dcID); @@ -168,12 +165,8 @@ angular.module('izhukov.mtproto.wrapper', ['izhukov.utils', 'izhukov.mtproto']) cachedExportPromise[dcID].then(function () { (cachedNetworker = networker).wrapApiCall(method, params, options).then(function (result) { deferred.resolve(result); - }, function (error) { - rejectPromise(error); - }); - }, function (error) { - rejectPromise(error); - }); + }, rejectPromise); + }, rejectPromise); } else if (error.code == 303) { var newDcID = error.type.match(/^(PHONE_MIGRATE_|NETWORK_MIGRATE_|USER_MIGRATE_)(\d+)/)[2]; @@ -187,9 +180,7 @@ angular.module('izhukov.mtproto.wrapper', ['izhukov.utils', 'izhukov.mtproto']) mtpGetNetworker(newDcID, options).then(function (networker) { networker.wrapApiCall(method, params, options).then(function (result) { deferred.resolve(result); - }, function (error) { - rejectPromise(error); - }); + }, rejectPromise); }); } } @@ -197,14 +188,14 @@ angular.module('izhukov.mtproto.wrapper', ['izhukov.utils', 'izhukov.mtproto']) rejectPromise(error); } }); - }, function (error) { - rejectPromise(error); - }); + }; - if (!(stack = (stack || (new Error()).stack))) { - try {window.unexistingFunction();} catch (e) { - stack = e.stack || ''; - } + if (dcID = (options.dcID || baseDcID)) { + mtpGetNetworker(dcID, options).then(performRequest, rejectPromise); + } else { + Storage.get('dc').then(function (baseDcID) { + mtpGetNetworker(dcID = baseDcID || 2, options).then(performRequest, rejectPromise); + }); } return deferred.promise; @@ -234,14 +225,12 @@ angular.module('izhukov.mtproto.wrapper', ['izhukov.utils', 'izhukov.mtproto']) var cachedFs = false; var cachedFsPromise = false; - var apiUploadPromise = $q.when(); var cachedSavePromises = {}; var cachedDownloadPromises = {}; var cachedDownloads = {}; var downloadPulls = {}; var downloadActives = {}; - var downloadLimit = 5; function downloadRequest(dcID, cb, activeDelta) { if (downloadPulls[dcID] === undefined) { @@ -251,7 +240,9 @@ angular.module('izhukov.mtproto.wrapper', ['izhukov.utils', 'izhukov.mtproto']) var downloadPull = downloadPulls[dcID]; var deferred = $q.defer(); downloadPull.push({cb: cb, deferred: deferred, activeDelta: activeDelta}); - downloadCheck(dcID); + setZeroTimeout(function () { + downloadCheck(dcID); + }); return deferred.promise; }; @@ -260,6 +251,7 @@ angular.module('izhukov.mtproto.wrapper', ['izhukov.utils', 'izhukov.mtproto']) function downloadCheck(dcID) { var downloadPull = downloadPulls[dcID]; + var downloadLimit = dcID == 'upload' ? 11 : 5; if (downloadActives[dcID] >= downloadLimit || !downloadPull || !downloadPull.length) { return false; @@ -383,6 +375,13 @@ angular.module('izhukov.mtproto.wrapper', ['izhukov.utils', 'izhukov.mtproto']) }); } + function getDownloadedFile(location, size) { + var fileStorage = getFileStorage(), + fileName = getFileName(location); + + return fileStorage.getFile(fileName, size); + } + function downloadFile (dcID, location, size, options) { if (!FileManager.isAvailable()) { return $q.reject({type: 'BROWSER_BLOB_NOT_SUPPORTED'}); @@ -464,7 +463,7 @@ angular.module('izhukov.mtproto.wrapper', ['izhukov.utils', 'izhukov.mtproto']) fileDownload: true, createNetworker: true }); - }, 6).then(function (result) { + }, 2).then(function (result) { writeFilePromise.then(function () { if (canceled) { return $q.when(); @@ -507,15 +506,23 @@ angular.module('izhukov.mtproto.wrapper', ['izhukov.utils', 'izhukov.mtproto']) } function uploadFile (file) { - var fileSize = file.size, - // partSize = fileSize > 102400 ? 65536 : 4096, - // partSize = fileSize > 102400 ? 524288 : 4096, - partSize = fileSize > 102400 ? 524288 : 32768, - isBigFile = fileSize >= 10485760, - totalParts = Math.ceil(fileSize / partSize), - canceled = false, - resolved = false, - doneParts = 0; + var fileSize = file.size, + isBigFile = fileSize >= 10485760, + canceled = false, + resolved = false, + doneParts = 0, + partSize = 262144, // 256 Kb + activeDelta = 2; + + if (fileSize > 67108864) { + partSize = 524288; + activeDelta = 4; + } + else if (fileSize < 102400) { + partSize = 32768; + activeDelta = 1; + } + var totalParts = Math.ceil(fileSize / partSize); if (totalParts > 1500) { return $q.reject({type: 'FILE_TOO_BIG'}); @@ -526,6 +533,7 @@ angular.module('izhukov.mtproto.wrapper', ['izhukov.utils', 'izhukov.mtproto']) errorHandler = function (error) { // console.error('Up Error', error); deferred.reject(error); + canceled = true; errorHandler = angular.noop; }, part = 0, @@ -539,35 +547,34 @@ angular.module('izhukov.mtproto.wrapper', ['izhukov.utils', 'izhukov.mtproto']) }; - var fileReadPromise = $q.when(); - for (offset = 0; offset < fileSize; offset += partSize) { (function (offset, part) { - fileReadPromise = fileReadPromise.then(function () { - var fileReadDeferred = $q.defer(); + downloadRequest('upload', function () { + var uploadDeferred = $q.defer(); var reader = new FileReader(); var blob = file.slice(offset, offset + partSize); reader.onloadend = function (e) { - if (canceled || e.target.readyState != FileReader.DONE) { + if (canceled) { + uploadDeferred.reject(); return; } - var apiCurPromise = apiUploadPromise = apiUploadPromise.then(function () { - return MtpApiManager.invokeApi(isBigFile ? 'upload.saveBigFilePart' : 'upload.saveFilePart', { - file_id: fileID, - file_part: part, - file_total_parts: totalParts, - bytes: bytesFromArrayBuffer(e.target.result) - }, { - startMaxLength: partSize + 256, - fileUpload: true - }); - }, errorHandler); - - apiCurPromise.then(function (result) { + if (e.target.readyState != FileReader.DONE) { + return; + } + MtpApiManager.invokeApi(isBigFile ? 'upload.saveBigFilePart' : 'upload.saveFilePart', { + file_id: fileID, + file_part: part, + file_total_parts: totalParts, + bytes: e.target.result + }, { + startMaxLength: partSize + 256, + fileUpload: true, + singleInRequest: true + }).then(function (result) { doneParts++; - fileReadDeferred.resolve(); + uploadDeferred.resolve(); if (doneParts >= totalParts) { deferred.resolve(resultInputFile); resolved = true; @@ -580,8 +587,8 @@ angular.module('izhukov.mtproto.wrapper', ['izhukov.utils', 'izhukov.mtproto']) reader.readAsArrayBuffer(blob); - return fileReadDeferred.promise; - }); + return uploadDeferred.promise; + }, activeDelta); })(offset, part++); } @@ -598,9 +605,87 @@ angular.module('izhukov.mtproto.wrapper', ['izhukov.utils', 'izhukov.mtproto']) return { getCachedFile: getCachedFile, + getDownloadedFile: getDownloadedFile, downloadFile: downloadFile, downloadSmallFile: downloadSmallFile, saveSmallFile: saveSmallFile, uploadFile: uploadFile }; }) + +.service('MtpSingleInstanceService', function (_, $rootScope, $interval, Storage, AppRuntimeManager, IdleManager, ErrorService, MtpNetworkerFactory) { + + var instanceID = nextRandomInt(0xFFFFFFFF); + var started = false; + var masterInstance = false; + var startTime = tsNow(); + var errorShowTime = 0; + + function start() { + if (!started) { + started = true; + + IdleManager.start(); + + startTime = tsNow(); + $rootScope.$watch('idle.isIDLE', checkInstance); + $interval(checkInstance, 5000); + checkInstance(); + + try { + $($window).on('beforeunload', clearInstance); + } catch (e) {}; + } + } + + function clearInstance () { + Storage.remove(masterInstance ? 'xt_instance' : 'xt_idle_instance'); + } + + function checkInstance() { + var time = tsNow(); + var idle = $rootScope.idle && $rootScope.idle.isIDLE; + var newInstance = {id: instanceID, idle: idle, time: time}; + + Storage.get('xt_instance', 'xt_idle_instance').then(function (result) { + var curInstance = result[0], + idleInstance = result[1]; + + if (!curInstance || + curInstance.time < time - 60000 || + curInstance.id == instanceID || + curInstance.idle || + !idle) { + + if (idleInstance) { + if (idleInstance.id == instanceID) { + Storage.remove('xt_idle_instance'); + } + else if (idleInstance.time > time - 10000 && + time > errorShowTime) { + + ErrorService.alert(_('error_modal_warning_title'), _('error_modal_multiple_open_tabs')); + errorShowTime += tsNow() + 60000; + } + } + Storage.set({xt_instance: newInstance}); + if (!masterInstance) { + MtpNetworkerFactory.startAll(); + } + masterInstance = true; + } else { + Storage.set({xt_idle_instance: newInstance}); + if (masterInstance) { + MtpNetworkerFactory.stopAll(); + } + masterInstance = false; + + } + }); + } + + return { + start: start + } +}) + diff --git a/app/js/lib/ng_utils.js b/app/js/lib/ng_utils.js index 82a98c61..61fa441b 100644 --- a/app/js/lib/ng_utils.js +++ b/app/js/lib/ng_utils.js @@ -33,7 +33,7 @@ angular.module('izhukov.utils', []) }) -.service('FileManager', function ($window, $timeout, $q) { +.service('FileManager', function ($window, $q, $timeout) { $window.URL = $window.URL || $window.webkitURL; $window.BlobBuilder = $window.BlobBuilder || $window.WebKitBlobBuilder || $window.MozBlobBuilder; @@ -98,10 +98,10 @@ angular.module('izhukov.utils', []) else { try { var blob = blobConstruct([bytesToArrayBuffer(bytes)]); + fileWriter.write(blob); } catch (e) { deferred.reject(e); } - fileWriter.write(blob); } return deferred.promise; @@ -150,7 +150,7 @@ angular.module('izhukov.utils', []) return false; } blobParts.push(blob); - $timeout(function () { + setZeroTimeout(function () { if (fakeFileWriter.onwriteend) { fakeFileWriter.onwriteend(); } @@ -484,35 +484,59 @@ angular.module('izhukov.utils', []) .service('CryptoWorker', function ($timeout, $q) { - var worker = window.Worker && new Worker('js/lib/crypto_worker.js') || false, + var webWorker = false, + naClEmbed = false, taskID = 0, - awaiting = {}; - - if (worker) { - worker.onmessage = function (e) { - var deferred = awaiting[e.data.taskID]; - if (deferred !== undefined) { - console.log(dT(), 'CW done'); - deferred.resolve(e.data.result); - delete awaiting[e.data.taskID]; + awaiting = {}, + webCrypto = window.crypto && (window.crypto.subtle || window.crypto.webkitSubtle) || window.msCrypto && window.msCrypto.subtle, + useSha1Crypto = webCrypto && webCrypto.digest !== undefined, + finalizeTask = function (taskID, result) { + var deferred = awaiting[taskID]; + if (deferred !== undefined) { + // console.log(dT(), 'CW done'); + deferred.resolve(result); + delete awaiting[taskID]; + } + }; + + if (navigator.mimeTypes['application/x-pnacl'] !== undefined) { + var listener = $('
').appendTo($('body'))[0]; + listener.addEventListener('load', function (e) { + naClEmbed = listener.firstChild; + console.log(dT(), 'NaCl ready'); + }, true); + listener.addEventListener('message', function (e) { + finalizeTask(e.data.taskID, e.data.result); + }, true); + listener.addEventListener('error', function (e) { + console.error('NaCl error', e); + }, true); + } + + if (window.Worker) { + var tmpWorker = new Worker('js/lib/crypto_worker.js'); + tmpWorker.onmessage = function (e) { + if (!webWorker) { + webWorker = tmpWorker; + } else { + finalizeTask(e.data.taskID, e.data.result); } }; - - worker.onerror = function(error) { + tmpWorker.onerror = function(error) { console.error('CW error', error, error.stack); - worker = false; + webWorker = false; }; } - function performTaskWorker (task, params) { - console.log(dT(), 'CW start', task); + function performTaskWorker (task, params, embed) { + // console.log(dT(), 'CW start', task); var deferred = $q.defer(); awaiting[taskID] = deferred; params.task = task; params.taskID = taskID; - worker.postMessage(params); + (embed || webWorker).postMessage(params); taskID++; @@ -521,39 +545,56 @@ angular.module('izhukov.utils', []) return { sha1Hash: function (bytes) { - if (worker && false) { // due overhead for data transfer - return performTaskWorker ('sha1-hash', {bytes: bytes}); + if (useSha1Crypto) { + // We don't use buffer since typedArray.subarray(...).buffer gives the whole buffer and not sliced one. webCrypto.digest supports typed array + var deferred = $q.defer(), + bytesTyped = Array.isArray(bytes) ? convertToUint8Array(bytes) : bytes; + // console.log(dT(), 'Native sha1 start'); + webCrypto.digest({name: 'SHA-1'}, bytesTyped).then(function (digest) { + // console.log(dT(), 'Native sha1 done'); + deferred.resolve(digest); + }, function (e) { + console.error('Crypto digest error', e); + useSha1Crypto = false; + deferred.resolve(sha1HashSync(bytes)); + }); + + return deferred.promise; } return $timeout(function () { - return sha1Hash(bytes); + return sha1HashSync(bytes); }); }, aesEncrypt: function (bytes, keyBytes, ivBytes) { - if (worker && false) { // due overhead for data transfer + if (naClEmbed) { return performTaskWorker('aes-encrypt', { - bytes: bytes, - keyBytes: keyBytes, - ivBytes: ivBytes - }); + bytes: addPadding(convertToArrayBuffer(bytes)), + keyBytes: convertToArrayBuffer(keyBytes), + ivBytes: convertToArrayBuffer(ivBytes) + }, naClEmbed); } return $timeout(function () { - return aesEncrypt(bytes, keyBytes, ivBytes); + return convertToArrayBuffer(aesEncryptSync(bytes, keyBytes, ivBytes)); }); }, aesDecrypt: function (encryptedBytes, keyBytes, ivBytes) { - if (worker && false) { // due overhead for data transfer + if (naClEmbed) { return performTaskWorker('aes-decrypt', { - encryptedBytes: encryptedBytes, - keyBytes: keyBytes, - ivBytes: ivBytes - }); + encryptedBytes: addPadding(convertToArrayBuffer(encryptedBytes)), + keyBytes: convertToArrayBuffer(keyBytes), + ivBytes: convertToArrayBuffer(ivBytes) + }, naClEmbed); } return $timeout(function () { - return aesDecrypt(encryptedBytes, keyBytes, ivBytes); + return convertToArrayBuffer(aesDecryptSync(encryptedBytes, keyBytes, ivBytes)); }); }, factorize: function (bytes) { - if (worker) { + bytes = convertToByteArray(bytes); + if (naClEmbed && bytes.length <= 8) { + return performTaskWorker('factorize', {bytes: bytes}, naClEmbed); + } + if (webWorker) { return performTaskWorker('factorize', {bytes: bytes}); } return $timeout(function () { @@ -561,7 +602,7 @@ angular.module('izhukov.utils', []) }); }, modPow: function (x, y, m) { - if (worker) { + if (webWorker) { return performTaskWorker('mod-pow', { x: x, y: y, @@ -575,6 +616,197 @@ angular.module('izhukov.utils', []) }; }) +.service('SearchIndexManager', function () { + var badCharsRe = /[`~!@#$%^&*()\-_=+\[\]\\|{}'";:\/?.>,<\s]+/g, + trimRe = /^\s+|\s$/g, + accentsReplace = { + a: /[åáâäà]/g, + e: /[éêëè]/g, + i: /[íîïì]/g, + o: /[óôöò]/g, + u: /[úûüù]/g, + c: /ç/g, + ss: /ß/g + } + + return { + createIndex: createIndex, + indexObject: indexObject, + cleanSearchText: cleanSearchText, + search: search + }; + + function createIndex () { + return { + shortIndexes: {}, + fullTexts: {} + } + } + + function cleanSearchText (text) { + text = text.replace(badCharsRe, ' ').replace(trimRe, '').toLowerCase(); + + for (var key in accentsReplace) { + if (accentsReplace.hasOwnProperty(key)) { + text = text.replace(accentsReplace[key], key); + } + } + + return text; + } + + function indexObject (id, searchText, searchIndex) { + if (searchIndex.fullTexts[id] !== undefined) { + return false; + } + + searchText = cleanSearchText(searchText); + + if (!searchText.length) { + return false; + } + + var shortIndexes = searchIndex.shortIndexes; + + searchIndex.fullTexts[id] = searchText; + + angular.forEach(searchText.split(' '), function(searchWord) { + var len = Math.min(searchWord.length, 3), + wordPart, i; + for (i = 1; i <= len; i++) { + wordPart = searchWord.substr(0, i); + if (shortIndexes[wordPart] === undefined) { + shortIndexes[wordPart] = [id]; + } else { + shortIndexes[wordPart].push(id); + } + } + }); + } + + function search (query, searchIndex) { + var shortIndexes = searchIndex.shortIndexes, + fullTexts = searchIndex.fullTexts; + + query = cleanSearchText(query); + + var queryWords = query.split(' '), + foundObjs = false, + newFoundObjs, i, j, searchText, found; + + for (i = 0; i < queryWords.length; i++) { + newFoundObjs = shortIndexes[queryWords[i].substr(0, 3)]; + if (!newFoundObjs) { + foundObjs = []; + break; + } + if (foundObjs === false || foundObjs.length > newFoundObjs.length) { + foundObjs = newFoundObjs; + } + } + + newFoundObjs = {}; + + for (j = 0; j < foundObjs.length; j++) { + found = true; + searchText = fullTexts[foundObjs[j]]; + for (i = 0; i < queryWords.length; i++) { + if (searchText.indexOf(queryWords[i]) == -1) { + found = false; + break; + } + } + if (found) { + newFoundObjs[foundObjs[j]] = true; + } + } + + return newFoundObjs; + } +}) + +.service('ExternalResourcesManager', function ($q, $http) { + var urlPromises = {}; + var twitterAttached = false; + + function downloadImage (url) { + if (urlPromises[url] !== undefined) { + return urlPromises[url]; + } + + return urlPromises[url] = $http.get(url, {responseType: 'blob', transformRequest: null}) + .then(function (response) { + window.URL = window.URL || window.webkitURL; + return window.URL.createObjectURL(response.data); + }); + } + + function attachTwitterScript () { + twitterAttached = true; + + $('