Browse Source

WIP

added new emoji tooltip class
added new emoji panel class
wip: textarea composer
master
Igor Zhukov 10 years ago
parent
commit
da10dce320
  1. 243
      app/css/app.css
  2. 61
      app/css/desktop.css
  3. BIN
      app/img/icons/General.png
  4. BIN
      app/img/icons/General_1x.png
  5. BIN
      app/img/icons/IconsetSmiles.png
  6. BIN
      app/img/icons/IconsetSmiles_1x.png
  7. 1
      app/index.html
  8. 38
      app/js/app.js
  9. 4
      app/js/controllers.js
  10. 10
      app/js/directives.js
  11. 109
      app/js/lib/ng_utils.js
  12. 170
      app/js/lib/utils.js
  13. 396
      app/js/message_composer.js
  14. 6
      app/js/services.js
  15. 26
      app/partials/desktop/emoji_btn_tooltip.html
  16. 11
      app/partials/desktop/im.html
  17. 2
      app/webogram.appcache

243
app/css/app.css

@ -771,7 +771,7 @@ a.tg_radio_on:hover i.icon-radio {
display: inline-block; display: inline-block;
vertical-align: top; vertical-align: top;
background: url(../img/icons/General.png) no-repeat -5px -10px; background: url(../img/icons/General.png) no-repeat -5px -10px;
background-size: 40px 678px; background-size: 40px 778px;
margin-right: 18px; margin-right: 18px;
} }
.icon-tg-title { .icon-tg-title {
@ -809,7 +809,7 @@ a.tg_radio_on:hover i.icon-radio {
display: inline-block; display: inline-block;
vertical-align: middle; vertical-align: middle;
background: url(../img/icons/General.png) no-repeat -18px -50px; background: url(../img/icons/General.png) no-repeat -18px -50px;
background-size: 40px 678px; background-size: 40px 778px;
margin-left: 12px; margin-left: 12px;
margin-top: -1px; margin-top: -1px;
} }
@ -1453,7 +1453,7 @@ div.im_message_video_thumb {
display: inline-block; display: inline-block;
line-height: 0; line-height: 0;
background: url(../img/icons/General.png) no-repeat -14px -509px; background: url(../img/icons/General.png) no-repeat -14px -509px;
background-size: 40px 678px; background-size: 40px 778px;
width: 12px; width: 12px;
height: 18px; height: 18px;
margin: 12px 15px; margin: 12px 15px;
@ -1476,7 +1476,7 @@ div.im_message_video_thumb {
width: 14px; width: 14px;
height: 16px; height: 16px;
background: url(../img/icons/General.png) no-repeat -13px -611px; background: url(../img/icons/General.png) no-repeat -13px -611px;
background-size: 40px 678px;; background-size: 40px 778px;;
margin: 13px 16px; margin: 13px 16px;
} }
.is_1x .im_message_file_button_dl_audio .im_message_file_button_icon { .is_1x .im_message_file_button_dl_audio .im_message_file_button_icon {
@ -1775,42 +1775,6 @@ a.im_message_fwd_photo {
line-height: 150%; line-height: 150%;
} }
span.emoji {
display: -moz-inline-box;
-moz-box-orient: vertical;
display: inline-block;
vertical-align: baseline;
*vertical-align: auto;
*zoom: 1;
*display: inline;
height: 18px;
width: 18px;
background-repeat: no-repeat;
text-indent: -9999px;
}
/* widths and heights calculated according to spritesheet dimensions and icon size */
.emoji-spritesheet-0 {
background-size: 486px 126px;
background-image: url('../img/emojisprite_0.png');
}
.emoji-spritesheet-1 {
background-size: 522px 72px;
background-image: url('../img/emojisprite_1.png');
}
.emoji-spritesheet-2 {
background-size: 594px 126px;
background-image: url('../img/emojisprite_2.png');
}
.emoji-spritesheet-3 {
background-size: 612px 54px;
background-image: url('../img/emojisprite_3.png');
}
.emoji-spritesheet-4 {
background-size: 612px 126px;
background-image: url('../img/emojisprite_4.png');
}
.im_history_not_selected, .im_history_not_selected,
.im_history_empty { .im_history_empty {
visibility: hidden; visibility: hidden;
@ -1995,116 +1959,189 @@ img.img_fullsize {
vertical-align: middle; vertical-align: middle;
margin: -3px 0 0 0; margin: -3px 0 0 0;
} }
.emoji-menu {
.composer_emoji_insert_btn {
display: block;
position: absolute;
right: 3px;
top: 2px;
cursor: pointer;
padding: 0;
width: 22px;
height: 22px;
margin-top: 1px;
}
.icon-emoji {
display: inline-block;
width: 22px;
height: 22px;
vertical-align: top;
background: url(../img/icons/General.png) no-repeat -9px -335px;
background-size: 40px 778px;
opacity: 0.8;
}
.composer_emoji_tooltip {
display: none;
position: absolute; position: absolute;
z-index: 999; z-index: 999;
width: 220px; width: 220px;
margin-left: -107px; margin-left: -100px;
margin-top: -252px; margin-top: -248px;
overflow: hidden;
border: 1px #dfdfdf solid; border: 1px #dfdfdf solid;
-webkit-border-radius: 3px; -webkit-border-radius: 3px;
-moz-border-radius: 3px; -moz-border-radius: 3px;
border-radius: 3px; border-radius: 3px;
overflow: hidden;
-webkit-box-shadow: 0px 1px 1px rgba(0, 0, 0, 0.1); -webkit-box-shadow: 0px 1px 1px rgba(0, 0, 0, 0.1);
-moz-box-shadow: 0px 1px 1px rgba(0, 0, 0, 0.1); -moz-box-shadow: 0px 1px 1px rgba(0, 0, 0, 0.1);
box-shadow: 0px 1px 1px rgba(0, 0, 0, 0.1); box-shadow: 0px 1px 1px rgba(0, 0, 0, 0.1);
}
.emoji-items-wrap1 {
background: #FFF; background: #FFF;
padding: 5px 2px 5px 5px; padding: 5px 2px 5px 5px;
} }
.emoji-items-wrap1 .emoji-menu-tabs { .icon-tooltip-tail {
background: #FFF;
width: 18px;
height: 18px;
-webkit-transform: rotate(45deg);
-moz-transform: rotate(45deg);
-ms-transform: rotate(45deg);
-o-transform: rotate(45deg);
transform: rotate(45deg);
display: inline-block;
-webkit-box-shadow: 1px 1px 1px rgba(0, 0, 0, 0.1);
-moz-box-shadow: 1px 1px 1px rgba(0, 0, 0, 0.1);
box-shadow: 1px 1px 1px rgba(0, 0, 0, 0.1);
border: 1px #dfdfdf solid;
border-width: 0 1px 1px 0;
-webkit-border-radius: 2px;
-moz-border-radius: 2px;
border-radius: 2px;
position: absolute;
bottom: -10px;
left: 50%;
margin-left: -9px;
}
.composer_emoji_tooltip_tabs {
width: 100%; width: 100%;
margin-bottom: 8px; margin-bottom: 8px;
margin-top: 3px; margin-top: 3px;
} }
.emoji-items-wrap1 .emoji-menu-tabs td { .composer_emoji_tooltip_tab {
text-align: center;
color: white;
line-height: 0; line-height: 0;
} color: white;
.emoji-menu-tabs .emoji-menu-tab {
display: inline-block; display: inline-block;
width: 24px; width: 24px;
height: 29px; height: 29px;
background: url(../img/icons/IconsetSmiles.png) no-repeat; background: url(../img/icons/IconsetSmiles.png) no-repeat;
background-size: 42px 350px; background-size: 42px 470px;
cursor: pointer;
opacity: 0.7;
margin: 0 3px;
} }
.is_1x .emoji-menu-tabs .emoji-menu-tab { .is_1x .composer_emoji_tooltip_tab {
background-image: url(../img/icons/IconsetSmiles_1x.png); background-image: url(../img/icons/IconsetSmiles_1x.png);
} }
.composer_emoji_tooltip_tab.active {
opacity: 1;
}
.emoji-menu-tabs .icon-recent {background-position: -9px -306px; } .composer_emoji_tooltip_tab_recent {background-position: -9px -306px; }
.emoji-menu-tabs .icon-recent-selected {background-position: -9px -277px; } .composer_emoji_tooltip_tab_recent.active {background-position: -9px -277px; }
.emoji-menu-tabs .icon-smile {background-position: -9px -34px; } .composer_emoji_tooltip_tab_smile {background-position: -9px -34px; }
.emoji-menu-tabs .icon-smile-selected {background-position: -9px -5px; } .composer_emoji_tooltip_tab_smile.active {background-position: -9px -5px; }
.emoji-menu-tabs .icon-flower {background-position: -9px -145px; } .composer_emoji_tooltip_tab_flower {background-position: -9px -145px; }
.emoji-menu-tabs .icon-flower-selected {background-position: -9px -118px; } .composer_emoji_tooltip_tab_flower.active {background-position: -9px -118px; }
.emoji-menu-tabs .icon-bell {background-position: -9px -89px; } .composer_emoji_tooltip_tab_bell {background-position: -9px -89px; }
.emoji-menu-tabs .icon-bell-selected {background-position: -9px -61px; } .composer_emoji_tooltip_tab_bell.active {background-position: -9px -61px; }
.emoji-menu-tabs .icon-car {background-position: -9px -196px; } .composer_emoji_tooltip_tab_car {background-position: -9px -196px; }
.emoji-menu-tabs .icon-car-selected {background-position: -9px -170px; } .composer_emoji_tooltip_tab_car.active {background-position: -9px -170px; }
.emoji-menu-tabs .icon-grid {background-position: -9px -248px; } .composer_emoji_tooltip_tab_grid {background-position: -9px -248px; }
.emoji-menu-tabs .icon-grid-selected {background-position: -9px -222px; } .composer_emoji_tooltip_tab_grid.active {background-position: -9px -222px; }
.emoji-menu-tabs .icon-recent,
.emoji-menu-tabs .icon-smile,
.emoji-menu-tabs .icon-flower,
.emoji-menu-tabs .icon-bell,
.emoji-menu-tabs .icon-car,
.emoji-menu-tabs .icon-grid {
opacity: 0.7;
}
.emoji-menu-tabs .icon-recent:hover,
.emoji-menu-tabs .icon-smile:hover,
.emoji-menu-tabs .icon-flower:hover,
.emoji-menu-tabs .icon-bell:hover,
.emoji-menu-tabs .icon-car:hover,
.emoji-menu-tabs .icon-grid:hover {
opacity: 1;
}
.composer_emoji_tooltip_tab_stickers {background-position: -9px -361px; }
.composer_emoji_tooltip_tab_stickers.active {background-position: -9px -333px; }
.emoji-menu .emoji-items-wrap { .composer_emoji_tooltip_content {
position: relative; position: relative;
height: 174px; height: 174px;
overflow: hidden;
overflow-y: auto;
} }
.emoji-menu .emoji-items {
padding-right: 8px;
outline: 0 !important; a.composer_emoji_btn {
}
.emoji-menu img {
width: 20px;
height: 20px;
vertical-align: middle;
border: 0 none;
}
.emoji-menu .emoji-items a {
margin: -1px 0 0 -1px; margin: -1px 0 0 -1px;
padding: 5px; padding: 5px;
display: block; display: block;
float: left; float: left;
border-radius: 2px; border-radius: 2px;
} }
.emoji-menu .emoji-items a:hover { a.composer_emoji_btn:hover {
background-color: #edf2f5; background-color: #edf2f5;
} }
.emoji-menu:after {
content: ' ';
display: block;
clear: left; .emoji {
display: -moz-inline-box;
-moz-box-orient: vertical;
display: inline-block;
vertical-align: baseline;
*vertical-align: auto;
*zoom: 1;
*display: inline;
height: 18px;
width: 18px;
background-repeat: no-repeat;
text-indent: -9999px;
} }
.emoji-menu a .label {
display: none; /* widths and heights calculated according to spritesheet dimensions and icon size */
.emoji-spritesheet-0 {
background-size: 486px 126px;
background-image: url('../img/emojisprite_0.png');
}
.emoji-spritesheet-1 {
background-size: 522px 72px;
background-image: url('../img/emojisprite_1.png');
}
.emoji-spritesheet-2 {
background-size: 594px 126px;
background-image: url('../img/emojisprite_2.png');
}
.emoji-spritesheet-3 {
background-size: 612px 54px;
background-image: url('../img/emojisprite_3.png');
} }
.emoji-spritesheet-4 {
background-size: 612px 126px;
background-image: url('../img/emojisprite_4.png');
}
.composer_emoji_btn .emoji {
width: 20px;
height: 20px;
vertical-align: middle;
border: 0 none;
display: inline-block;
}
.composer_emoji_btn .emoji-spritesheet-0 {background-size: 540px 140px;}
.composer_emoji_btn .emoji-spritesheet-1 {background-size: 580px 80px;}
.composer_emoji_btn .emoji-spritesheet-2 {background-size: 660px 140px;}
.composer_emoji_btn .emoji-spritesheet-3 {background-size: 680px 60px;}
.composer_emoji_btn .emoji-spritesheet-4 {background-size: 680px 140px;}
.error_modal_window .modal-dialog { .error_modal_window .modal-dialog {

61
app/css/desktop.css

@ -171,7 +171,7 @@
margin-right: 38px; margin-right: 38px;
display: inline-block; display: inline-block;
background: url(../img/icons/General.png) no-repeat -10px -111px; background: url(../img/icons/General.png) no-repeat -10px -111px;
background-size: 40px 678px; background-size: 40px 778px;
vertical-align: top; vertical-align: top;
margin-top: 3px; margin-top: 3px;
} }
@ -183,7 +183,7 @@
display: inline-block; display: inline-block;
vertical-align: top; vertical-align: top;
background: url(../img/icons/General.png) no-repeat -11px -135px; background: url(../img/icons/General.png) no-repeat -11px -135px;
background-size: 40px 678px; background-size: 40px 778px;
} }
.icon-settings { .icon-settings {
width: 20px; width: 20px;
@ -193,7 +193,7 @@
display: inline-block; display: inline-block;
vertical-align: top; vertical-align: top;
background: url(../img/icons/General.png) no-repeat -10px -163px; background: url(../img/icons/General.png) no-repeat -10px -163px;
background-size: 40px 678px; background-size: 40px 778px;
} }
.icon-faq { .icon-faq {
width: 20px; width: 20px;
@ -203,7 +203,7 @@
display: inline-block; display: inline-block;
vertical-align: top; vertical-align: top;
background: url(../img/icons/General.png) no-repeat -10px -637px; background: url(../img/icons/General.png) no-repeat -10px -637px;
background-size: 40px 678px; background-size: 40px 778px;
} }
.icon-about { .icon-about {
width: 21px; width: 21px;
@ -212,7 +212,7 @@
display: inline-block; display: inline-block;
vertical-align: top; vertical-align: top;
background: url(../img/icons/General.png) no-repeat -10px -193px; background: url(../img/icons/General.png) no-repeat -10px -193px;
background-size: 40px 678px; background-size: 40px 778px;
} }
.is_1x .icon-new-group, .is_1x .icon-new-group,
.is_1x .icon-contacts, .is_1x .icon-contacts,
@ -315,7 +315,7 @@
.icon-filter-audio { .icon-filter-audio {
display: inline-block; display: inline-block;
background: url(../img/icons/General.png) no-repeat 0 0; background: url(../img/icons/General.png) no-repeat 0 0;
background-size: 40px 678px; background-size: 40px 778px;
margin-right: 12px; margin-right: 12px;
vertical-align: top; vertical-align: top;
} }
@ -722,26 +722,21 @@ a.footer_link.active:active {
background: inherit; background: inherit;
} }
.im_emoji_quick_select_area { .composer_emoji_panel {
display: block; display: block;
height: 30px; height: 30px;
overflow: hidden; overflow: hidden;
max-width: 210px; max-width: 210px;
} }
.composer_emoji_panel a {
.im_emoji_quick_select_area a {
display: inline-block; display: inline-block;
padding: 5px; padding: 5px;
outline: 0; outline: 0;
border-radius: 2px; border-radius: 2px;
} }
/*.composer_emoji_panel a:hover {
.im_emoji_quick_select_area a:hover {
background-color: #edf2f5; background-color: #edf2f5;
} }*/
.im_emoji_quick_select_area a .label {
display: none;
}
.im_message_selected .im_message_date, .im_message_selected .im_message_date,
.im_message_selected .im_message_document_size, .im_message_selected .im_message_document_size,
@ -940,7 +935,8 @@ a.im_panel_peer_photo .peer_initials {
margin-left: 36px; margin-left: 36px;
} }
.im_emoji_btn { /*.composer_emoji_insert_btn {
display: block;
position: absolute; position: absolute;
right: 3px; right: 3px;
top: 2px; top: 2px;
@ -950,28 +946,43 @@ a.im_panel_peer_photo .peer_initials {
width: 22px; width: 22px;
height: 22px; height: 22px;
margin-top: 1px; margin-top: 1px;
} }*/
.icon-emoji { /*.icon-emoji {
display: inline-block; display: inline-block;
width: 22px; width: 22px;
height: 22px; height: 22px;
vertical-align: top; vertical-align: top;
background: url(../img/icons/General.png) no-repeat -9px -335px; background: url(../img/icons/General.png) no-repeat -9px -335px;
background-size: 40px 678px; background-size: 40px 778px;
opacity: 0.8; opacity: 0.8;
} }*/
.is_1x .icon-emoji { .is_1x .icon-emoji {
background-image: url(../img/icons/General_1x.png); background-image: url(../img/icons/General_1x.png);
} }
.im_emoji_btn:hover .icon-emoji { .composer_emoji_insert_btn:hover .icon-emoji {
opacity: 1; opacity: 1;
} }
.im_emoji_btn:active .icon-emoji, .composer_emoji_insert_btn:active .icon-emoji,
.im_emoji_btn.on .icon-emoji { .composer_emoji_insert_btn.on .icon-emoji {
background-position: -9px -367px; background-position: -9px -367px;
opacity: 1; opacity: 1;
} }
.im_send_field_wrap { .im_send_field_wrap {
margin-bottom: 15px; margin-bottom: 15px;
} }
@ -1014,7 +1025,7 @@ a.im_panel_peer_photo .peer_initials {
height: 17px; height: 17px;
vertical-align: top; vertical-align: top;
background: url(../img/icons/General.png) no-repeat -11px -455px; background: url(../img/icons/General.png) no-repeat -11px -455px;
background-size: 40px 678px; background-size: 40px 778px;
opacity: 0.8; opacity: 0.8;
margin: 0; margin: 0;
} }
@ -1050,7 +1061,7 @@ a.im_panel_peer_photo .peer_initials {
height: 18px; height: 18px;
vertical-align: top; vertical-align: top;
background: url(../img/icons/General.png) no-repeat -10px -399px; background: url(../img/icons/General.png) no-repeat -10px -399px;
background-size: 40px 678px; background-size: 40px 778px;
opacity: 0.8; opacity: 0.8;
} }
.is_1x .icon-camera { .is_1x .icon-camera {

BIN
app/img/icons/General.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 13 KiB

BIN
app/img/icons/General_1x.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

BIN
app/img/icons/IconsetSmiles.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 9.8 KiB

BIN
app/img/icons/IconsetSmiles_1x.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

1
app/index.html

@ -84,6 +84,7 @@
PRODUCTION_ONLY_END--> PRODUCTION_ONLY_END-->
<script type="text/javascript" src="js/directives.js"></script> <script type="text/javascript" src="js/directives.js"></script>
<script type="text/javascript" src="js/message_composer.js"></script>
<script type="text/javascript" src="js/directives_mobile.js"></script> <script type="text/javascript" src="js/directives_mobile.js"></script>
<!-- endbuild --> <!-- endbuild -->

38
app/js/app.js

@ -27,25 +27,25 @@ angular.module('myApp', [
]). ]).
config(['$locationProvider', '$routeProvider', '$compileProvider', 'StorageProvider', function($locationProvider, $routeProvider, $compileProvider, StorageProvider) { config(['$locationProvider', '$routeProvider', '$compileProvider', 'StorageProvider', function($locationProvider, $routeProvider, $compileProvider, StorageProvider) {
var icons = {}, reverseIcons = {}, i, j, hex, name, dataItem, row, column, totalColumns; // var icons = {}, reverseIcons = {}, i, j, hex, name, dataItem, row, column, totalColumns;
for (j = 0; j < Config.EmojiCategories.length; j++) { // for (j = 0; j < Config.EmojiCategories.length; j++) {
totalColumns = Config.EmojiCategorySpritesheetDimens[j][1]; // totalColumns = Config.EmojiCategorySpritesheetDimens[j][1];
for (i = 0; i < Config.EmojiCategories[j].length; i++) { // for (i = 0; i < Config.EmojiCategories[j].length; i++) {
dataItem = Config.Emoji[Config.EmojiCategories[j][i]]; // dataItem = Config.Emoji[Config.EmojiCategories[j][i]];
name = dataItem[1][0]; // name = dataItem[1][0];
row = Math.floor(i / totalColumns); // row = Math.floor(i / totalColumns);
column = (i % totalColumns); // column = (i % totalColumns);
icons[':' + name + ':'] = [j, row, column, ':'+name+':']; // icons[':' + name + ':'] = [j, row, column, ':'+name+':'];
reverseIcons[name] = dataItem[0]; // reverseIcons[name] = dataItem[0];
} // }
} // }
$.emojiarea.spritesheetPath = 'img/emojisprite_!.png'; // $.emojiarea.spritesheetPath = 'img/emojisprite_!.png';
$.emojiarea.spritesheetDimens = Config.EmojiCategorySpritesheetDimens; // $.emojiarea.spritesheetDimens = Config.EmojiCategorySpritesheetDimens;
$.emojiarea.iconSize = 20; // $.emojiarea.iconSize = 20;
$.emojiarea.icons = icons; // $.emojiarea.icons = icons;
$.emojiarea.reverseIcons = reverseIcons; // $.emojiarea.reverseIcons = reverseIcons;
$compileProvider.imgSrcSanitizationWhitelist(/^\s*(https?|ftp|file|blob|filesystem|chrome-extension|app):|data:image\//); $compileProvider.imgSrcSanitizationWhitelist(/^\s*(https?|ftp|file|blob|filesystem|chrome-extension|app):|data:image\//);
$compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|ftp|file|mailto|blob|filesystem|chrome-extension|app):|data:image\//); $compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|ftp|file|mailto|blob|filesystem|chrome-extension|app):|data:image\//);

4
app/js/controllers.js

@ -2992,7 +2992,7 @@ angular.module('myApp.controllers', ['myApp.i18n'])
}) })
.controller('CountrySelectModalController', function ($scope, $modalInstance, $rootScope, SearchIndexManager, _) { .controller('CountrySelectModalController', function ($scope, $modalInstance, $rootScope, _) {
$scope.search = {}; $scope.search = {};
$scope.slice = {limit: 20, limitDelta: 20} $scope.slice = {limit: 20, limitDelta: 20}
@ -3035,7 +3035,7 @@ angular.module('myApp.controllers', ['myApp.i18n'])
}) })
.controller('PhonebookModalController', function ($scope, $modalInstance, $rootScope, AppUsersManager, PhonebookContactsService, SearchIndexManager, ErrorService) { .controller('PhonebookModalController', function ($scope, $modalInstance, $rootScope, AppUsersManager, PhonebookContactsService, ErrorService) {
$scope.search = {}; $scope.search = {};
$scope.phonebook = []; $scope.phonebook = [];

10
app/js/directives.js

@ -1071,6 +1071,16 @@ angular.module('myApp.directives', ['myApp.filters'])
}; };
function link ($scope, element, attrs) { function link ($scope, element, attrs) {
var emojiButton = $('.composer_emoji_insert_btn', element)[0];
new EmojiTooltip(emojiButton);
var emojiPanel = $('.composer_emoji_panel', element)[0];
new EmojiPanel(emojiPanel);
return;
var messageField = $('textarea', element)[0], var messageField = $('textarea', element)[0],
fileSelects = $('input', element), fileSelects = $('input', element),
dropbox = $('.im_send_dropbox_wrap', element)[0], dropbox = $('.im_send_dropbox_wrap', element)[0],

109
app/js/lib/ng_utils.js

@ -734,115 +734,6 @@ 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) { .service('ExternalResourcesManager', function ($q, $http) {
var urlPromises = {}; var urlPromises = {};

170
app/js/lib/utils.js

@ -58,9 +58,64 @@ function onCtrlEnter (textarea, cb) {
}); });
} }
function setFieldSelection(field, from, to) {
field = $(field)[0];
try {
field.focus();
if (from === undefined || from === false) {
from = field.value.length;
}
if (to === undefined || to === false) {
to = from;
}
if (field.createTextRange) {
var range = field.createTextRange();
range.collapse(true);
range.moveEnd('character', to);
range.moveStart('character', from);
range.select();
}
else if (field.setSelectionRange) {
field.setSelectionRange(from, to);
}
} catch(e) {}
}
function getFieldSelection (field) {
if (field.selectionStart) {
return field.selectionStart;
}
else if (!document.selection) {
return 0;
}
var c = "\001",
sel = document.selection.createRange(),
txt = sel.text,
dup = sel.duplicate(),
len = 0;
try {
dup.moveToElementText(field);
} catch(e) {
return 0;
}
sel.text = txt + c;
len = dup.text.indexOf(c);
sel.moveStart('character',-1);
sel.text = '';
// if (browser.msie && len == -1) {
// return field.value.length;
// }
return len;
}
function onContentLoaded (cb) { function onContentLoaded (cb) {
setTimeout(cb, 0); setTimeout(cb, 0);
}; }
function tsNow (seconds) { function tsNow (seconds) {
var t = +new Date() + (window.tsOffset || 0); var t = +new Date() + (window.tsOffset || 0);
@ -177,3 +232,116 @@ function versionCompare (ver1, ver2) {
return 0; return 0;
} }
(function (global) {
var badCharsRe = /[`~!@#$%^&*()\-_=+\[\]\\|{}'";:\/?.>,<\s]+/g,
trimRe = /^\s+|\s$/g,
accentsReplace = {
a: /[åáâäà]/g,
e: /[éêëè]/g,
i: /[íîïì]/g,
o: /[óôöò]/g,
u: /[úûüù]/g,
c: /ç/g,
ss: /ß/g
};
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;
}
global.SearchIndexManager = {
createIndex: createIndex,
indexObject: indexObject,
cleanSearchText: cleanSearchText,
search: search
};
})(window);

396
app/js/message_composer.js

@ -0,0 +1,396 @@
/*!
* Webogram v0.3.9 - messaging web application for MTProto
* https://github.com/zhukov/webogram
* Copyright (C) 2014 Igor Zhukov <igor.beatle@gmail.com>
* https://github.com/zhukov/webogram/blob/master/LICENSE
*/
'use strict';
/* EmojiHelper */
(function (global, emojis, categories, spritesheets) {
var emojis = {};
var shortcuts = {};
var spritesheetPositions = {};
var index = false;
var popular = 'joy,kissing_heart,heart,heart_eyes,blush,grin,+1,relaxed,pensive,smile,sob,kiss,unamused,flushed,stuck_out_tongue_winking_eye,see_no_evil,wink,smiley,cry,stuck_out_tongue_closed_eyes,scream,rage,smirk,disappointed,sweat_smile,kissing_closed_eyes,speak_no_evil,relieved,grinning,yum,laughing,ok_hand,neutral_face,confused'.split(',');
var i, j, code, shortcut, emoji, row, column, totalColumns;
var len1, len2;
for (i = 0, len1 = categories.length; i < len1; i++) {
totalColumns = spritesheets[i][1];
for (j = 0, len2 = categories[i].length; j < len2; j++) {
code = categories[i][j];
emoji = Config.Emoji[code];
shortcut = emoji[1][0];
emojis[code] = [emoji[0], shortcut];
shortcuts[shortcut] = code;
spritesheetPositions[code] = [i, j, Math.floor(j / totalColumns), j % totalColumns];
}
}
function getPopularEmoji (callback) {
ConfigStorage.get('emojis_popular', function (popEmojis) {
var result = [];
if (popEmojis && popEmojis.length) {
for (var i = 0, len = popEmojis.length; i < len; i++) {
result.push({code: popEmojis[i][0], rate: popEmojis[i][1]});
}
callback(result);
return;
};
ConfigStorage.get('emojis_recent', function (recentEmojis) {
recentEmojis = recentEmojis || popular || [];
var shortcut, code;
for (var i = 0, len = recentEmojis.length; i < len; i++) {
shortcut = recentEmojis[i];
if (Array.isArray(shortcut)) {
shortcut = shortcut[0];
}
if (shortcut.charAt(0) == ':') {
shortcut = shortcut.substr(1, shortcut.length - 2);
}
if (code = shortcuts[shortcut]) {
result.push({code: code, rate: 1});
}
}
callback(result);
});
});
}
function pushPopularEmoji (code) {
getPopularEmoji(function (popularEmoji) {
var exists = false;
var count = popularEmoji.length;
var result = [];
for (var i = 0; i < count; i++) {
if (popularEmoji[i].code == code) {
exists = true;
popularEmoji[i].rate++;
}
result.push([popularEmoji[i].code, popularEmoji[i].rate]);
}
if (exists) {
result.sort(function (a, b) {
return b[1] - a[1];
});
} else {
if (result.length > 41) {
result = result.slice(0, 41);
}
result.push([code, 1]);
}
ConfigStorage.set({emojis_popular: result});
});
}
function indexEmojis () {
if (index === false) {
index = SearchIndexManager.createIndex();
var shortcut;
for (shortcut in shortcuts) {
if (shortcuts.hasOwnProperty(shortcut)) {
SearchIndexManager.indexObject(shortcuts[shortcut], shortcut, index);
}
}
}
}
function searchEmojis (q) {
indexEmojis();
return SearchIndexManager.search(q, index);
}
global.EmojiHelper = {
emojis: emojis,
shortcuts: shortcuts,
spritesheetPositions: spritesheetPositions,
getPopularEmoji: getPopularEmoji,
pushPopularEmoji: pushPopularEmoji,
indexEmojis: indexEmojis,
searchEmojis: searchEmojis
};
})(window, Config.Emoji, Config.EmojiCategories, Config.EmojiCategorySpritesheetDimens);
function EmojiTooltip (btnEl, options) {
options = options || {};
var self = this;
this.btnEl = $(btnEl);
this.onEmojiSelected = options.onEmojiSelected;
$(this.btnEl).on('mouseenter mouseleave', function (e) {
self.isOverBtn = e.type == 'mouseenter';
self.createTooltip();
if (self.isOverBtn) {
self.onMouseEnter(true);
} else {
self.onMouseLeave(true);
}
});
}
EmojiTooltip.prototype.onMouseEnter = function (triggerShow) {
if (this.hideTimeout) {
clearTimeout(this.hideTimeout);
delete this.hideTimeout;
}
else if (triggerShow && !this.showTimeout) {
this.showTimeout = setTimeout(this.show.bind(this), 500);
}
};
EmojiTooltip.prototype.onMouseLeave = function (triggerUnshow) {
if (!this.hideTimeout) {
var self = this;
this.hideTimeout = setTimeout(function () {
self.hide();
}, 500);
}
else if (triggerUnshow && this.showTimeout) {
clearTimeout(this.showTimeout);
delete this.showTimeout;
}
};
EmojiTooltip.prototype.createTooltip = function () {
if (this.tooltipEl) {
return false;
}
var self = this;
this.tooltipEl = $('<div class="composer_emoji_tooltip noselect"><div class="composer_emoji_tooltip_tabs"></div><div class="composer_emoji_tooltip_content clearfix"></div><div class="composer_emoji_tooltip_footer"><a class="composer_emoji_tooltip_settings"></a></div><i class="icon icon-tooltip-tail"></i></div>').appendTo(document.body);
this.tabsEl = $('.composer_emoji_tooltip_tabs', this.tooltip);
this.contentEl = $('.composer_emoji_tooltip_content', this.tooltip);
this.footerEl = $('.composer_emoji_tooltip_footer', this.tooltip);
this.settingsEl = $('.composer_emoji_tooltip_settings', this.tooltip);
angular.forEach(['recent', 'smile', 'flower', 'bell', 'car', 'grid', 'stickers'], function (tabName, tabIndex) {
$('<a class="composer_emoji_tooltip_tab composer_emoji_tooltip_tab_' + tabName + '"></a>')
.on('mousedown', function (e) {
self.selectTab(tabIndex);
return cancelEvent(e);
})
.appendTo(self.tabsEl);
});
this.contentEl.on('mousedown', function (e) {
e = e.originalEvent || e;
var target = $(e.target), code;
if (target.hasClass('emoji')) {
target = $(target[0].parentNode);
}
if (code = target.attr('data-code')) {
if (self.onEmojiSelected) {
self.onEmojiSelected(code);
}
EmojiHelper.pushPopularEmoji(code);
}
return cancelEvent(e);
});
this.tooltipEl.on('mouseenter mouseleave', function (e) {
console.log(dT(), e.type);
if (e.type == 'mouseenter') {
self.onMouseEnter();
} else {
self.onMouseLeave();
}
});
this.selectTab(0);
return true;
}
EmojiTooltip.prototype.selectTab = function (tab) {
if (this.tab === tab) {
return false;
}
$('.active', this.tabsEl).removeClass('active');
this.tab = tab;
$(this.tabsEl[0].childNodes[tab]).addClass('active');
this.updateTabContents();
};
EmojiTooltip.prototype.updateTabContents = function (tab) {
var html = [];
var self = this;
var iconSize = Config.Mobile ? 26 : 20;
if (this.tab > 0) {
var categoryIndex = this.tab - 1;
var emoticonCodes = Config.EmojiCategories[categoryIndex];
var totalColumns = Config.EmojiCategorySpritesheetDimens[categoryIndex][1];
var count = emoticonCodes.length;
var emoticonCode, emoticonData, i, x, y;
for (i = 0; i < count; i++) {
emoticonCode = emoticonCodes[i];
emoticonData = Config.Emoji[emoticonCode];
x = iconSize * (i % totalColumns);
y = iconSize * Math.floor(i / totalColumns);
html.push('<a class="composer_emoji_btn" title=":' + encodeEntities(emoticonData[1][0]) + ':" data-code="' + encodeEntities(emoticonCode) + '"><i class="emoji emoji-spritesheet-' + categoryIndex + '" style="background-position: -' + x + 'px -' + y + 'px;"></i></a>');
}
this.contentEl.html(html.join(''));
}
else {
EmojiHelper.getPopularEmoji(function (popularEmoji) {
var emoticonCode, emoticonData, spritesheet, pos, categoryIndex;
var count = popularEmoji.length;
var i, x, y;
for (i = 0; i < count; i++) {
emoticonCode = popularEmoji[i].code;
if (emoticonData = Config.Emoji[emoticonCode]) {
spritesheet = EmojiHelper.spritesheetPositions[emoticonCode];
categoryIndex = spritesheet[0];
pos = spritesheet[1];
x = iconSize * spritesheet[3];
y = iconSize * spritesheet[2];
html.push('<a class="composer_emoji_btn" title=":' + encodeEntities(emoticonData[1][0]) + ':" data-code="' + encodeEntities(emoticonCode) + '"><i class="emoji emoji-spritesheet-' + categoryIndex + '" style="background-position: -' + x + 'px -' + y + 'px;"></i></a>');
}
}
self.contentEl.html(html.join(''));
});
}
};
EmojiTooltip.prototype.updatePosition = function () {
var offset = this.btnEl.offset();
this.tooltipEl.css({top: offset.top, left: offset.left});
};
EmojiTooltip.prototype.show = function () {
this.updatePosition();
this.tooltipEl.show();
delete this.showTimeout;
};
EmojiTooltip.prototype.hide = function () {
this.tooltipEl.hide();
delete this.hideTimeout;
};
function EmojiPanel (containerEl, options) {
options = options || {};
// var self = this;
this.containerEl = $(containerEl);
this.onEmojiSelected = options.onEmojiSelected;
this.containerEl.on('mousedown', function (e) {
e = e.originalEvent || e;
var target = $(e.target), code;
if (target.hasClass('emoji')) {
target = $(target[0].parentNode);
}
if (code = target.attr('data-code')) {
if (self.onEmojiSelected) {
self.onEmojiSelected(code);
}
EmojiHelper.pushPopularEmoji(code);
}
return cancelEvent(e);
});
this.update();
}
EmojiPanel.prototype.update = function () {
var html = [];
var self = this;
var iconSize = Config.Mobile ? 26 : 20;
EmojiHelper.getPopularEmoji(function (popularEmoji) {
var emoticonCode, emoticonData, spritesheet, pos, categoryIndex;
var count = popularEmoji.length;
var i, x, y;
for (i = 0; i < count; i++) {
emoticonCode = popularEmoji[i].code;
if (emoticonData = Config.Emoji[emoticonCode]) {
spritesheet = EmojiHelper.spritesheetPositions[emoticonCode];
categoryIndex = spritesheet[0];
pos = spritesheet[1];
x = iconSize * spritesheet[3];
y = iconSize * spritesheet[2];
html.push('<a class="composer_emoji_btn" title=":' + encodeEntities(emoticonData[1][0]) + ':" data-code="' + encodeEntities(emoticonCode) + '"><i class="emoji emoji-spritesheet-' + categoryIndex + '" style="background-position: -' + x + 'px -' + y + 'px;"></i></a>');
}
}
self.containerEl.html(html.join(''));
});
}
function MessageComposer (textarea, options) {
this.textareaEl = $(textarea);
this.textareaEl.on('keyup keydown', this.onKeyEvent.bind(this));
this.textareaEl.on('focus blur', this.onFocusBlur.bind(this));
this.isActive = false;
}
MessageComposer.prototype.onKeyEvent = function (e) {
var self = this;
if (e.type == 'keyup') {
this.checkAutocomplete();
}
}
MessageComposer.prototype.checkAutocomplete = function (e) {
var pos = getFieldSelection(e.target);
var value = this.textareaEl[0].value.substr(0, pos);
var matches = value.match(/:([A-Za-z_]*)$/);
if (matches) {
if (matches[1]) {
var found = EmojiHelper.searchEmojis(matches[1]);
self.showEmojiSuggestions(found);
} else {
EmojiHelper.getPopularEmoji(function (found) {
self.showEmojiSuggestions(found);
});
}
}
else {
self.hideSuggestions();
}
}
MessageComposer.prototype.onFocusBlur = function (e) {
this.isActive = e.type == 'focus';
if (!this.isActive) {
this.hideSuggestions();
}
}
MessageComposer.prototype.showEmojiSuggestions = function (codes) {
this.autocompleteShown = true;
}
MessageComposer.prototype.hideSuggestions = function () {
delete this.autocompleteShown;
}

6
app/js/services.js

@ -11,7 +11,7 @@
angular.module('myApp.services', ['myApp.i18n', 'izhukov.utils']) angular.module('myApp.services', ['myApp.i18n', 'izhukov.utils'])
.service('AppUsersManager', function ($rootScope, $modal, $modalStack, $filter, $q, qSync, MtpApiFileManager, MtpApiManager, RichTextProcessor, SearchIndexManager, ErrorService, Storage, _) { .service('AppUsersManager', function ($rootScope, $modal, $modalStack, $filter, $q, qSync, MtpApiFileManager, MtpApiManager, RichTextProcessor, ErrorService, Storage, _) {
var users = {}, var users = {},
usernames = {}, usernames = {},
cachedPhotoLocations = {}, cachedPhotoLocations = {},
@ -507,7 +507,7 @@ angular.module('myApp.services', ['myApp.i18n', 'izhukov.utils'])
}) })
.service('AppChatsManager', function ($rootScope, $modal, _, MtpApiFileManager, MtpApiManager, AppUsersManager, RichTextProcessor, SearchIndexManager) { .service('AppChatsManager', function ($rootScope, $modal, _, MtpApiFileManager, MtpApiManager, AppUsersManager, RichTextProcessor) {
var chats = {}, var chats = {},
cachedPhotoLocations = {}; cachedPhotoLocations = {};
@ -687,7 +687,7 @@ angular.module('myApp.services', ['myApp.i18n', 'izhukov.utils'])
} }
}) })
.service('AppMessagesManager', function ($q, $rootScope, $location, $filter, ApiUpdatesManager, AppUsersManager, AppChatsManager, AppPeersManager, AppPhotosManager, AppVideoManager, AppDocsManager, AppAudioManager, MtpApiManager, MtpApiFileManager, RichTextProcessor, NotificationsManager, SearchIndexManager, PeersSelectService, Storage, FileManager, TelegramMeWebService, StatusManager, _) { .service('AppMessagesManager', function ($q, $rootScope, $location, $filter, ApiUpdatesManager, AppUsersManager, AppChatsManager, AppPeersManager, AppPhotosManager, AppVideoManager, AppDocsManager, AppAudioManager, MtpApiManager, MtpApiFileManager, RichTextProcessor, NotificationsManager, PeersSelectService, Storage, FileManager, TelegramMeWebService, StatusManager, _) {
var messagesStorage = {}; var messagesStorage = {};
var messagesForHistory = {}; var messagesForHistory = {};

26
app/partials/desktop/emoji_btn_tooltip.html

@ -0,0 +1,26 @@
<div class="composer_emoji_btn_wrap">
<div class="composer_emoji_tooltip_wrap">
<div class="composer_emoji_tooltip">
<div class="composer_emoji_tooltip_tabs">
<a ng-repeat="tab in ::tabs" href="" class="composer_emoji_tooltip_tab" ng-class="['composer_emoji_tooltip_tab_' + tab, curTab == tab ? 'active' : '']" ng-click="tabSelect(tab)"></a>
</div>
<div class="composer_emoji_content" ng-switch="curTab == 'stickers'">
<div ng-switch-when="true" class="composer_emoji_content_emoticons">
<a class="composer_emoticon_option" href="" ng-repeat="emoji in emojis" ng-click="emojiSelect(emoji)"></a>
</div>
<div ng-switch-default class="composer_emoji_content_stickers">
<a class="composer_sticker_option" href="" ng-repeat="sticker in stickers" ng-click="stickerSelect(sticker)"></a>
</div>
</div>
</div>
</div>
<a class="composer_emoji_btn">
<i class="icon icon-emoji"></i>
</a>
</div>

11
app/partials/desktop/im.html

@ -171,14 +171,13 @@
</a> </a>
<a class="pull-left im_panel_own_photo" my-peer-photolink="ownID" img-class="im_panel_own_photo" watch="true" ng-click="openSettings()" no-open="true"></a> <a class="pull-left im_panel_own_photo" my-peer-photolink="ownID" img-class="im_panel_own_photo" watch="true" ng-click="openSettings()" no-open="true"></a>
<form my-send-form draft-message="draftMessage" class="im_send_form" ng-class="{im_send_form_empty: !draftMessage.text.length}"> <form my-send-form draft-message="draftMessage" class="im_send_form" ng-class="{im_send_form_empty: !draftMessage.text.length}" message-composer>
<div class="im_send_field_wrap"> <div class="im_send_field_wrap">
<div class="im_emoji_btn pull-right" title="{{'im_emoji_btn_title' | i18n}}"> <a class="composer_emoji_insert_btn"><i class="icon icon-emoji"></i></a>
<i class="icon icon-emoji"></i>
</div>
<div class="im_send_dropbox_wrap" my-i18n="im_photos_drop_text"></div> <div class="im_send_dropbox_wrap" my-i18n="im_photos_drop_text"></div>
<textarea ng-model="draftMessage.text" placeholder="{{'im_message_field_placeholder' | i18n}}" class="form-control im_message_field no_outline"></textarea> <textarea ng-model="draftMessage.text" placeholder="{{'im_message_field_placeholder' | i18n}}" class="form-control im_message_field no_outline" message-composer-field></textarea>
</div> </div>
<div class="clearfix"> <div class="clearfix">
@ -194,7 +193,7 @@
<i class="icon icon-camera"></i> <i class="icon icon-camera"></i>
</div> </div>
<div class="im_emoji_quick_select_area"></div> <div class="composer_emoji_panel" message-composer-emoji-recents></div>
</div> </div>
</form> </form>

2
app/webogram.appcache

@ -1,6 +1,6 @@
CACHE MANIFEST CACHE MANIFEST
# 55 # 57
NETWORK: NETWORK:
* *

Loading…
Cancel
Save