summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--doc/developer/tests/test-emoticons.md930
-rw-r--r--web/react/components/edit_channel_purpose_modal.jsx9
-rw-r--r--web/react/components/search_autocomplete.jsx157
-rw-r--r--web/react/components/user_profile.jsx2
-rw-r--r--web/react/utils/highlight.jsx51
-rw-r--r--web/react/utils/markdown.jsx351
-rw-r--r--web/react/utils/utils.jsx9
-rw-r--r--web/web.go5
8 files changed, 1411 insertions, 103 deletions
diff --git a/doc/developer/tests/test-emoticons.md b/doc/developer/tests/test-emoticons.md
new file mode 100644
index 000000000..9ea1afec0
--- /dev/null
+++ b/doc/developer/tests/test-emoticons.md
@@ -0,0 +1,930 @@
+# Emoticon Testing
+
+The below text can be used to check for regressions in changes to text processing. Paste the below text into messages and check that emoticons continue to render after text processing changes.
+
+```
+# Emoticon - Punctuation
+
+:) :-)
+;) ;-)
+:o :O
+:-o :-O
+:] :-]
+:d :-D
+x-d x-D
+:p :-P
+:@
+:( :-(
+:'(
+:* :-*
+:/ :-/
+:s :-s
+:| :-|
+:$ :-$
+:-x
+<3
+:+1:
+:-1:
+```
+
+```
+# Emoticons - People
+
+:bowtie:
+:smile:
+:laughing:
+:blush:
+:smiley:
+:relaxed:
+:smirk:
+:heart_eyes:
+:kissing_heart:
+:kissing_closed_eyes:
+:flushed:
+:relieved:
+:satisfied:
+:grin:
+:wink:
+:stuck_out_tongue_winking_eye:
+:stuck_out_tongue_closed_eyes:
+:grinning:
+:kissing:
+:kissing_smiling_eyes:
+:stuck_out_tongue:
+:sleeping:
+:worried:
+:frowning:
+:anguished:
+:open_mouth:
+:grimacing:
+:confused:
+:hushed:
+:expressionless:
+:unamused:
+:sweat_smile:
+:sweat:
+:disappointed_relieved:
+:weary:
+:pensive:
+:disappointed:
+:confounded:
+:fearful:
+:cold_sweat:
+:persevere:
+:cry:
+:sob:
+:joy:
+:astonished:
+:scream:
+:neckbeard:
+:tired_face:
+:angry:
+:rage:
+:triumph:
+:sleepy:
+:yum:
+:mask:
+:sunglasses:
+:dizzy_face:
+:imp:
+:smiling_imp:
+:neutral_face:
+:no_mouth:
+:innocent:
+:alien:
+:yellow_heart:
+:blue_heart:
+:purple_heart:
+:heart:
+:green_heart:
+:broken_heart:
+:heartbeat:
+:heartpulse:
+:two_hearts:
+:revolving_hearts:
+:cupid:
+:sparkling_heart:
+:sparkles:
+:star:
+:star2:
+:dizzy:
+:boom:
+:collision:
+:anger:
+:exclamation:
+:question:
+:grey_exclamation:
+:grey_question:
+:zzz:
+:dash:
+:sweat_drops:
+:notes:
+:musical_note:
+:fire:
+:hankey:
+:poop:
+:shit:
+:+1:
+:thumbsup:
+:-1:
+:thumbsdown:
+:ok_hand:
+:punch:
+:facepunch:
+:fist:
+:v:
+:wave:
+:hand:
+:raised_hand:
+:open_hands:
+:point_up:
+:point_down:
+:point_left:
+:point_right:
+:raised_hands:
+:pray:
+:point_up_2:
+:clap:
+:muscle:
+:metal:
+:fu:
+:runner:
+:running:
+:couple:
+:family:
+:two_men_holding_hands:
+:two_women_holding_hands:
+:dancer:
+:dancers:
+:ok_woman:
+:no_good:
+:information_desk_person:
+:raising_hand:
+:bride_with_veil:
+:person_with_pouting_face:
+:person_frowning:
+:bow:
+:couplekiss:
+:couple_with_heart:
+:massage:
+:haircut:
+:nail_care:
+:boy:
+:girl:
+:woman:
+:man:
+:baby:
+:older_woman:
+:older_man:
+:person_with_blond_hair:
+:man_with_gua_pi_mao:
+:man_with_turban:
+:construction_worker:
+:cop:
+:angel:
+:princess:
+:smiley_cat:
+:smile_cat:
+:heart_eyes_cat:
+:kissing_cat:
+:smirk_cat:
+:scream_cat:
+:crying_cat_face:
+:joy_cat:
+:pouting_cat:
+:japanese_ogre:
+:japanese_goblin:
+:see_no_evil:
+:hear_no_evil:
+:speak_no_evil:
+:guardsman:
+:skull:
+:feet:
+:lips:
+:kiss:
+:droplet:
+:ear:
+:eyes:
+:nose:
+:tongue:
+:love_letter:
+:bust_in_silhouette:
+:busts_in_silhouette:
+:speech_balloon:
+:thought_balloon:
+:feelsgood:
+:finnadie:
+:goberserk:
+:godmode:
+:hurtrealbad:
+:rage1:
+:rage2:
+:rage3:
+:rage4:
+:suspect:
+:trollface:
+```
+
+```
+# Emoticons - Nature
+
+:sunny:
+:umbrella:
+:cloud:
+:snowflake:
+:snowman:
+:zap:
+:cyclone:
+:foggy:
+:ocean:
+:cat:
+:dog:
+:mouse:
+:hamster:
+:rabbit:
+:wolf:
+:frog:
+:tiger:
+:koala:
+:bear:
+:pig:
+:pig_nose:
+:cow:
+:boar:
+:monkey_face:
+:monkey:
+:horse:
+:racehorse:
+:camel:
+:sheep:
+:elephant:
+:panda_face:
+:snake:
+:bird:
+:baby_chick:
+:hatched_chick:
+:hatching_chick:
+:chicken:
+:penguin:
+:turtle:
+:bug:
+:honeybee:
+:ant:
+:beetle:
+:snail:
+:octopus:
+:tropical_fish:
+:fish:
+:whale:
+:whale2:
+:dolphin:
+:cow2:
+:ram:
+:rat:
+:water_buffalo:
+:tiger2:
+:rabbit2:
+:dragon:
+:goat:
+:rooster:
+:dog2:
+:pig2:
+:mouse2:
+:ox:
+:dragon_face:
+:blowfish:
+:crocodile:
+:dromedary_camel:
+:leopard:
+:cat2:
+:poodle:
+:paw_prints:
+:bouquet:
+:cherry_blossom:
+:tulip:
+:four_leaf_clover:
+:rose:
+:sunflower:
+:hibiscus:
+:maple_leaf:
+:leaves:
+:fallen_leaf:
+:herb:
+:mushroom:
+:cactus:
+:palm_tree:
+:evergreen_tree:
+:deciduous_tree:
+:chestnut:
+:seedling:
+:blossom:
+:ear_of_rice:
+:shell:
+:globe_with_meridians:
+:sun_with_face:
+:full_moon_with_face:
+:new_moon_with_face:
+:new_moon:
+:waxing_crescent_moon:
+:first_quarter_moon:
+:waxing_gibbous_moon:
+:full_moon:
+:waning_gibbous_moon:
+:last_quarter_moon:
+:waning_crescent_moon:
+:last_quarter_moon_with_face:
+:first_quarter_moon_with_face:
+:crescent_moon:
+:earth_africa:
+:earth_americas:
+:earth_asia:
+:volcano:
+:milky_way:
+:partly_sunny:
+:octocat:
+:squirrel:
+```
+
+```
+# Emoticons - Objects
+
+:bamboo:
+:gift_heart:
+:dolls:
+:school_satchel:
+:mortar_board:
+:flags:
+:fireworks:
+:sparkler:
+:wind_chime:
+:rice_scene:
+:jack_o_lantern:
+:ghost:
+:santa:
+:christmas_tree:
+:gift:
+:bell:
+:no_bell:
+:tanabata_tree:
+:tada:
+:confetti_ball:
+:balloon:
+:crystal_ball:
+:cd:
+:dvd:
+:floppy_disk:
+:camera:
+:video_camera:
+:movie_camera:
+:computer:
+:tv:
+:iphone:
+:phone:
+:telephone:
+:telephone_receiver:
+:pager:
+:fax:
+:minidisc:
+:vhs:
+:sound:
+:speaker:
+:mute:
+:loudspeaker:
+:mega:
+:hourglass:
+:hourglass_flowing_sand:
+:alarm_clock:
+:watch:
+:radio:
+:satellite:
+:loop:
+:mag:
+:mag_right:
+:unlock:
+:lock:
+:lock_with_ink_pen:
+:closed_lock_with_key:
+:key:
+:bulb:
+:flashlight:
+:high_brightness:
+:low_brightness:
+:electric_plug:
+:battery:
+:calling:
+:email:
+:mailbox:
+:postbox:
+:bath:
+:bathtub:
+:shower:
+:toilet:
+:wrench:
+:nut_and_bolt:
+:hammer:
+:seat:
+:moneybag:
+:yen:
+:dollar:
+:pound:
+:euro:
+:credit_card:
+:money_with_wings:
+:e-mail:
+:inbox_tray:
+:outbox_tray:
+:envelope:
+:incoming_envelope:
+:postal_horn:
+:mailbox_closed:
+:mailbox_with_mail:
+:mailbox_with_no_mail:
+:package:
+:door:
+:smoking:
+:bomb:
+:gun:
+:hocho:
+:pill:
+:syringe:
+:page_facing_up:
+:page_with_curl:
+:bookmark_tabs:
+:bar_chart:
+:chart_with_upwards_trend:
+:chart_with_downwards_trend:
+:scroll:
+:clipboard:
+:calendar:
+:date:
+:card_index:
+:file_folder:
+:open_file_folder:
+:scissors:
+:pushpin:
+:paperclip:
+:black_nib:
+:pencil2:
+:straight_ruler:
+:triangular_ruler:
+:closed_book:
+:green_book:
+:blue_book:
+:orange_book:
+:notebook:
+:notebook_with_decorative_cover:
+:ledger:
+:books:
+:bookmark:
+:name_badge:
+:microscope:
+:telescope:
+:newspaper:
+:football:
+:basketball:
+:soccer:
+:baseball:
+:tennis:
+:8ball:
+:8ball:
+:rugby_football:
+:bowling:
+:golf:
+:mountain_bicyclist:
+:bicyclist:
+:horse_racing:
+:snowboarder:
+:swimmer:
+:surfer:
+:ski:
+:spades:
+:hearts:
+:clubs:
+:diamonds:
+:gem:
+:ring:
+:trophy:
+:musical_score:
+:musical_keyboard:
+:violin:
+:space_invader:
+:video_game:
+:black_joker:
+:flower_playing_cards:
+:game_die:
+:dart:
+:mahjong:
+:clapper:
+:memo:
+:pencil:
+:book:
+:art:
+:microphone:
+:headphones:
+:trumpet:
+:saxophone:
+:guitar:
+:shoe:
+:sandal:
+:high_heel:
+:lipstick:
+:boot:
+:shirt:
+:tshirt:
+:necktie:
+:womans_clothes:
+:dress:
+:running_shirt_with_sash:
+:jeans:
+:kimono:
+:bikini:
+:ribbon:
+:tophat:
+:crown:
+:womans_hat:
+:mans_shoe:
+:closed_umbrella:
+:briefcase:
+:handbag:
+:pouch:
+:purse:
+:eyeglasses:
+:fishing_pole_and_fish:
+:coffee:
+:tea:
+:sake:
+:baby_bottle:
+:beer:
+:beers:
+:cocktail:
+:tropical_drink:
+:wine_glass:
+:fork_and_knife:
+:pizza:
+:hamburger:
+:fries:
+:poultry_leg:
+:meat_on_bone:
+:spaghetti:
+:curry:
+:fried_shrimp:
+:bento:
+:sushi:
+:fish_cake:
+:rice_ball:
+:rice_cracker:
+:rice:
+:ramen:
+:stew:
+:oden:
+:dango:
+:egg:
+:bread:
+:doughnut:
+:custard:
+:icecream:
+:ice_cream:
+:shaved_ice:
+:birthday:
+:cake:
+:cookie:
+:chocolate_bar:
+:candy:
+:lollipop:
+:honey_pot:
+:apple:
+:green_apple:
+:tangerine:
+:lemon:
+:cherries:
+:grapes:
+:watermelon:
+:strawberry:
+:peach:
+:melon:
+:banana:
+:pear:
+:pineapple:
+:sweet_potato:
+:eggplant:
+:tomato:
+:corn:
+```
+
+```
+# Emoticons - Places
+
+:house:
+:house_with_garden:
+:school:
+:office:
+:post_office:
+:hospital:
+:bank:
+:convenience_store:
+:love_hotel:
+:hotel:
+:wedding:
+:church:
+:department_store:
+:european_post_office:
+:city_sunrise:
+:city_sunset:
+:japanese_castle:
+:european_castle:
+:tent:
+:factory:
+:tokyo_tower:
+:japan:
+:mount_fuji:
+:sunrise_over_mountains:
+:sunrise:
+:stars:
+:statue_of_liberty:
+:bridge_at_night:
+:carousel_horse:
+:rainbow:
+:ferris_wheel:
+:fountain:
+:roller_coaster:
+:ship:
+:speedboat:
+:boat:
+:sailboat:
+:rowboat:
+:anchor:
+:rocket:
+:airplane:
+:helicopter:
+:steam_locomotive:
+:tram:
+:mountain_railway:
+:bike:
+:aerial_tramway:
+:suspension_railway:
+:mountain_cableway:
+:tractor:
+:blue_car:
+:oncoming_automobile:
+:car:
+:red_car:
+:taxi:
+:oncoming_taxi:
+:articulated_lorry:
+:bus:
+:oncoming_bus:
+:rotating_light:
+:police_car:
+:oncoming_police_car:
+:fire_engine:
+:ambulance:
+:minibus:
+:truck:
+:train:
+:station:
+:train2:
+:bullettrain_front:
+:bullettrain_side:
+:light_rail:
+:monorail:
+:railway_car:
+:trolleybus:
+:ticket:
+:fuelpump:
+:vertical_traffic_light:
+:traffic_light:
+:warning:
+:construction:
+:beginner:
+:atm:
+:slot_machine:
+:busstop:
+:barber:
+:hotsprings:
+:checkered_flag:
+:crossed_flags:
+:izakaya_lantern:
+:moyai:
+:circus_tent:
+:performing_arts:
+:round_pushpin:
+:triangular_flag_on_post:
+:jp:
+:kr:
+:cn:
+:us:
+:fr:
+:es:
+:it:
+:ru:
+:gb:
+:uk:
+:de:
+```
+
+```
+# Emoticons - Symbols
+
+:one:
+:two:
+:three:
+:four:
+:five:
+:six:
+:seven:
+:eight:
+:nine:
+:keycap_ten:
+:1234:
+:zero:
+:hash:
+:symbols:
+:arrow_backward:
+:arrow_down:
+:arrow_forward:
+:arrow_left:
+:capital_abcd:
+:abcd:
+:abc:
+:arrow_lower_left:
+:arrow_lower_right:
+:arrow_right:
+:arrow_up:
+:arrow_upper_left:
+:arrow_upper_right:
+:arrow_double_down:
+:arrow_double_up:
+:arrow_down_small:
+:arrow_heading_down:
+:arrow_heading_up:
+:leftwards_arrow_with_hook:
+:arrow_right_hook:
+:left_right_arrow:
+:arrow_up_down:
+:arrow_up_small:
+:arrows_clockwise:
+:arrows_counterclockwise:
+:rewind:
+:fast_forward:
+:information_source:
+:ok:
+:twisted_rightwards_arrows:
+:repeat:
+:repeat_one:
+:new:
+:top:
+:up:
+:cool:
+:free:
+:ng:
+:cinema:
+:koko:
+:signal_strength:
+:u5272:
+:u5408:
+:u55b6:
+:u6307:
+:u6708:
+:u6709:
+:u6e80:
+:u7121:
+:u7533:
+:u7a7a:
+:u7981:
+:sa:
+:restroom:
+:mens:
+:womens:
+:baby_symbol:
+:no_smoking:
+:parking:
+:wheelchair:
+:metro:
+:baggage_claim:
+:accept:
+:wc:
+:potable_water:
+:put_litter_in_its_place:
+:secret:
+:congratulations:
+:m:
+:passport_control:
+:left_luggage:
+:customs:
+:ideograph_advantage:
+:cl:
+:sos:
+:id:
+:no_entry_sign:
+:underage:
+:no_mobile_phones:
+:do_not_litter:
+:non-potable_water:
+:no_bicycles:
+:no_pedestrians:
+:children_crossing:
+:no_entry:
+:eight_spoked_asterisk:
+:sparkle:
+:eight_pointed_black_star:
+:heart_decoration:
+:vs:
+:vibration_mode:
+:mobile_phone_off:
+:chart:
+:currency_exchange:
+:aries:
+:taurus:
+:gemini:
+:cancer:
+:leo:
+:virgo:
+:libra:
+:scorpius:
+:sagittarius:
+:capricorn:
+:aquarius:
+:pisces:
+:ophiuchus:
+:six_pointed_star:
+:negative_squared_cross_mark:
+:a:
+:b:
+:ab:
+:o2:
+:diamond_shape_with_a_dot_inside:
+:recycle:
+:end:
+:back:
+:on:
+:soon:
+:clock1:
+:clock130:
+:clock10:
+:clock1030:
+:clock11:
+:clock1130:
+:clock12:
+:clock1230:
+:clock2:
+:clock230:
+:clock3:
+:clock330:
+:clock4:
+:clock430:
+:clock5:
+:clock530:
+:clock6:
+:clock630:
+:clock7:
+:clock730:
+:clock8:
+:clock830:
+:clock9:
+:clock930:
+:heavy_dollar_sign:
+:copyright:
+:registered:
+:tm:
+:x:
+:heavy_exclamation_mark:
+:bangbang:
+:interrobang:
+:o:
+:heavy_multiplication_x:
+:heavy_plus_sign:
+:heavy_minus_sign:
+:heavy_division_sign:
+:white_flower:
+:100:
+:heavy_check_mark:
+:ballot_box_with_check:
+:radio_button:
+:link:
+:curly_loop:
+:wavy_dash:
+:part_alternation_mark:
+:trident:
+:black_small_square:
+:white_small_square:
+:black_medium_small_square:
+:white_medium_small_square:
+:black_medium_square:
+:white_medium_square:
+:black_large_square:
+:white_large_square:
+:white_check_mark:
+:black_square_button:
+:white_square_button:
+:black_circle:
+:white_circle:
+:red_circle:
+:large_blue_circle:
+:large_blue_diamond:
+:large_orange_diamond:
+:small_blue_diamond:
+:small_orange_diamond:
+:small_red_triangle:
+:small_red_triangle_down:
+:shipit:
+```
diff --git a/web/react/components/edit_channel_purpose_modal.jsx b/web/react/components/edit_channel_purpose_modal.jsx
index 4cb96a3ff..65e8183de 100644
--- a/web/react/components/edit_channel_purpose_modal.jsx
+++ b/web/react/components/edit_channel_purpose_modal.jsx
@@ -3,6 +3,8 @@
const AsyncClient = require('../utils/async_client.jsx');
const Client = require('../utils/client.jsx');
+const Utils = require('../utils/utils.jsx');
+
const Modal = ReactBootstrap.Modal;
export default class EditChannelPurposeModal extends React.Component {
@@ -75,11 +77,6 @@ export default class EditChannelPurposeModal extends React.Component {
title = <span>{'Edit Purpose for '}<span className='name'>{this.props.channel.display_name}</span></span>;
}
- let channelTerm = 'Channel';
- if (this.props.channel.channelType === 'P') {
- channelTerm = 'Group';
- }
-
return (
<Modal
className='modal-edit-channel-purpose'
@@ -93,7 +90,7 @@ export default class EditChannelPurposeModal extends React.Component {
</Modal.Title>
</Modal.Header>
<Modal.Body>
- <p>{`Describe how this ${channelTerm} should be used.`}</p>
+ <p>{`Describe how this ${Utils.getChannelTerm(this.props.channel.channelType)} should be used.`}</p>
<textarea
ref='purpose'
className='form-control no-resize'
diff --git a/web/react/components/search_autocomplete.jsx b/web/react/components/search_autocomplete.jsx
index 03e14ec49..736919697 100644
--- a/web/react/components/search_autocomplete.jsx
+++ b/web/react/components/search_autocomplete.jsx
@@ -3,14 +3,15 @@
const ChannelStore = require('../stores/channel_store.jsx');
const KeyCodes = require('../utils/constants.jsx').KeyCodes;
+const Popover = ReactBootstrap.Popover;
const UserStore = require('../stores/user_store.jsx');
const Utils = require('../utils/utils.jsx');
+const Constants = require('../utils/constants.jsx');
const patterns = new Map([
['channels', /\b(?:in|channel):\s*(\S*)$/i],
['users', /\bfrom:\s*(\S*)$/i]
]);
-const Popover = ReactBootstrap.Popover;
export default class SearchAutocomplete extends React.Component {
constructor(props) {
@@ -22,8 +23,13 @@ export default class SearchAutocomplete extends React.Component {
this.handleKeyDown = this.handleKeyDown.bind(this);
this.completeWord = this.completeWord.bind(this);
+ this.getSelection = this.getSelection.bind(this);
+ this.scrollToItem = this.scrollToItem.bind(this);
this.updateSuggestions = this.updateSuggestions.bind(this);
+ this.renderChannelSuggestion = this.renderChannelSuggestion.bind(this);
+ this.renderUserSuggestion = this.renderUserSuggestion.bind(this);
+
this.state = {
show: false,
mode: '',
@@ -37,9 +43,18 @@ export default class SearchAutocomplete extends React.Component {
$(document).on('click', this.handleDocumentClick);
}
- componentDidUpdate() {
- $(ReactDOM.findDOMNode(this.refs.searchPopover)).find('.popover-content').perfectScrollbar();
- $(ReactDOM.findDOMNode(this.refs.searchPopover)).find('.popover-content').css('max-height', $(window).height() - 200);
+ componentDidUpdate(prevProps, prevState) {
+ const content = $(ReactDOM.findDOMNode(this.refs.searchPopover)).find('.popover-content');
+
+ if (this.state.show) {
+ if (!prevState.show) {
+ content.perfectScrollbar();
+ content.css('max-height', $(window).height() - 200);
+ }
+
+ // keep the keyboard selection visible when scrolling
+ this.scrollToItem(this.getSelection());
+ }
}
componentWillUnmount() {
@@ -51,7 +66,7 @@ export default class SearchAutocomplete extends React.Component {
}
handleDocumentClick(e) {
- const container = $(ReactDOM.findDOMNode(this.refs.container));
+ const container = $(ReactDOM.findDOMNode(this.refs.searchPopover));
if (!(container.is(e.target) || container.has(e.target).length > 0)) {
this.setState({
@@ -111,15 +126,7 @@ export default class SearchAutocomplete extends React.Component {
} else if (e.which === KeyCodes.ENTER || e.which === KeyCodes.SPACE) {
e.preventDefault();
- this.completeSelectedWord();
- }
- }
-
- completeSelectedWord() {
- if (this.state.mode === 'channels') {
- this.completeWord(this.state.suggestions[this.state.selection].name);
- } else if (this.state.mode === 'users') {
- this.completeWord(this.state.suggestions[this.state.selection].username);
+ this.completeWord(this.getSelection());
}
}
@@ -135,6 +142,40 @@ export default class SearchAutocomplete extends React.Component {
});
}
+ getSelection() {
+ if (this.state.mode === 'channels') {
+ return this.state.suggestions[this.state.selection].name;
+ } else if (this.state.mode === 'users') {
+ return this.state.suggestions[this.state.selection].username;
+ }
+
+ return '';
+ }
+
+ scrollToItem(itemName) {
+ const content = $(ReactDOM.findDOMNode(this.refs.searchPopover)).find('.popover-content');
+ const visibleContentHeight = content[0].clientHeight;
+ const actualContentHeight = content[0].scrollHeight;
+
+ if (this.state.suggestions.length > 0 && visibleContentHeight < actualContentHeight) {
+ const contentTop = content.scrollTop();
+ const contentTopPadding = parseInt(content.css('padding-top'), 10);
+ const contentBottomPadding = parseInt(content.css('padding-top'), 10);
+
+ const item = $(this.refs[itemName]);
+ const itemTop = item[0].offsetTop - parseInt(item.css('margin-top'), 10);
+ const itemBottom = item[0].offsetTop + item.height() + parseInt(item.css('margin-bottom'), 10);
+
+ if (itemTop - contentTopPadding < contentTop) {
+ // the item is off the top of the visible space
+ content.scrollTop(itemTop - contentTopPadding);
+ } else if (itemBottom + contentTopPadding + contentBottomPadding > contentTop + visibleContentHeight) {
+ // the item has gone off the bottom of the visible space
+ content.scrollTop(itemBottom - visibleContentHeight + contentTopPadding + contentBottomPadding);
+ }
+ }
+ }
+
updateSuggestions(mode, filter) {
let suggestions = [];
@@ -193,6 +234,46 @@ export default class SearchAutocomplete extends React.Component {
});
}
+ renderChannelSuggestion(channel) {
+ let className = 'search-autocomplete__item';
+ if (channel.name === this.getSelection()) {
+ className += ' selected';
+ }
+
+ return (
+ <div
+ key={channel.name}
+ ref={channel.name}
+ onClick={this.handleClick.bind(this, channel.name)}
+ className={className}
+ >
+ {channel.name}
+ </div>
+ );
+ }
+
+ renderUserSuggestion(user) {
+ let className = 'search-autocomplete__item';
+ if (user.username === this.getSelection()) {
+ className += ' selected';
+ }
+
+ return (
+ <div
+ key={user.username}
+ ref={user.username}
+ onClick={this.handleClick.bind(this, user.username)}
+ className={className}
+ >
+ <img
+ className='profile-img rounded'
+ src={'/api/v1/users/' + user.id + '/image?time=' + user.update_at}
+ />
+ {user.username}
+ </div>
+ );
+ }
+
render() {
if (!this.state.show || this.state.suggestions.length === 0) {
return null;
@@ -201,45 +282,33 @@ export default class SearchAutocomplete extends React.Component {
let suggestions = [];
if (this.state.mode === 'channels') {
- suggestions = this.state.suggestions.map((channel, index) => {
- let className = 'search-autocomplete__item';
- if (this.state.selection === index) {
- className += ' selected';
- }
-
- return (
+ const publicChannels = this.state.suggestions.filter((channel) => channel.type === Constants.OPEN_CHANNEL);
+ if (publicChannels.length > 0) {
+ suggestions.push(
<div
- key={channel.name}
- ref={channel.name}
- onClick={this.handleClick.bind(this, channel.name)}
- className={className}
+ key='public-channel-divider'
+ className='search-autocomplete__divider'
>
- {channel.name}
+ {'Public ' + Utils.getChannelTerm(Constants.OPEN_CHANNEL) + 's'}
</div>
);
- });
- } else if (this.state.mode === 'users') {
- suggestions = this.state.suggestions.map((user, index) => {
- let className = 'search-autocomplete__item';
- if (this.state.selection === index) {
- className += ' selected';
- }
+ suggestions = suggestions.concat(publicChannels.map(this.renderChannelSuggestion));
+ }
- return (
+ const privateChannels = this.state.suggestions.filter((channel) => channel.type === Constants.PRIVATE_CHANNEL);
+ if (privateChannels.length > 0) {
+ suggestions.push(
<div
- key={user.username}
- ref={user.username}
- onClick={this.handleClick.bind(this, user.username)}
- className={className}
+ key='private-channel-divider'
+ className='search-autocomplete__divider'
>
- <img
- className='profile-img rounded'
- src={'/api/v1/users/' + user.id + '/image?time=' + user.update_at}
- />
- {user.username}
+ {'Private ' + Utils.getChannelTerm(Constants.PRIVATE_CHANNEL) + 's'}
</div>
);
- });
+ suggestions = suggestions.concat(privateChannels.map(this.renderChannelSuggestion));
+ }
+ } else if (this.state.mode === 'users') {
+ suggestions = this.state.suggestions.map(this.renderUserSuggestion);
}
return (
diff --git a/web/react/components/user_profile.jsx b/web/react/components/user_profile.jsx
index 5ef2e8a19..c4402ae23 100644
--- a/web/react/components/user_profile.jsx
+++ b/web/react/components/user_profile.jsx
@@ -54,7 +54,7 @@ export default class UserProfile extends React.Component {
}
}
render() {
- var name = Utils.displayUsername(this.state.profile.id);
+ var name = this.state.profile.username;
if (this.props.overwriteName) {
name = this.props.overwriteName;
}
diff --git a/web/react/utils/highlight.jsx b/web/react/utils/highlight.jsx
new file mode 100644
index 000000000..68fef7930
--- /dev/null
+++ b/web/react/utils/highlight.jsx
@@ -0,0 +1,51 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+const highlightJs = require('highlight.js/lib/highlight.js');
+const highlightJsDiff = require('highlight.js/lib/languages/diff.js');
+const highlightJsApache = require('highlight.js/lib/languages/apache.js');
+const highlightJsMakefile = require('highlight.js/lib/languages/makefile.js');
+const highlightJsHttp = require('highlight.js/lib/languages/http.js');
+const highlightJsJson = require('highlight.js/lib/languages/json.js');
+const highlightJsMarkdown = require('highlight.js/lib/languages/markdown.js');
+const highlightJsJavascript = require('highlight.js/lib/languages/javascript.js');
+const highlightJsCss = require('highlight.js/lib/languages/css.js');
+const highlightJsNginx = require('highlight.js/lib/languages/nginx.js');
+const highlightJsObjectivec = require('highlight.js/lib/languages/objectivec.js');
+const highlightJsPython = require('highlight.js/lib/languages/python.js');
+const highlightJsXml = require('highlight.js/lib/languages/xml.js');
+const highlightJsPerl = require('highlight.js/lib/languages/perl.js');
+const highlightJsBash = require('highlight.js/lib/languages/bash.js');
+const highlightJsPhp = require('highlight.js/lib/languages/php.js');
+const highlightJsCoffeescript = require('highlight.js/lib/languages/coffeescript.js');
+const highlightJsCs = require('highlight.js/lib/languages/cs.js');
+const highlightJsCpp = require('highlight.js/lib/languages/cpp.js');
+const highlightJsSql = require('highlight.js/lib/languages/sql.js');
+const highlightJsGo = require('highlight.js/lib/languages/go.js');
+const highlightJsRuby = require('highlight.js/lib/languages/ruby.js');
+const highlightJsJava = require('highlight.js/lib/languages/java.js');
+const highlightJsIni = require('highlight.js/lib/languages/ini.js');
+
+highlightJs.registerLanguage('diff', highlightJsDiff);
+highlightJs.registerLanguage('apache', highlightJsApache);
+highlightJs.registerLanguage('makefile', highlightJsMakefile);
+highlightJs.registerLanguage('http', highlightJsHttp);
+highlightJs.registerLanguage('json', highlightJsJson);
+highlightJs.registerLanguage('markdown', highlightJsMarkdown);
+highlightJs.registerLanguage('javascript', highlightJsJavascript);
+highlightJs.registerLanguage('css', highlightJsCss);
+highlightJs.registerLanguage('nginx', highlightJsNginx);
+highlightJs.registerLanguage('objectivec', highlightJsObjectivec);
+highlightJs.registerLanguage('python', highlightJsPython);
+highlightJs.registerLanguage('xml', highlightJsXml);
+highlightJs.registerLanguage('perl', highlightJsPerl);
+highlightJs.registerLanguage('bash', highlightJsBash);
+highlightJs.registerLanguage('php', highlightJsPhp);
+highlightJs.registerLanguage('coffeescript', highlightJsCoffeescript);
+highlightJs.registerLanguage('cs', highlightJsCs);
+highlightJs.registerLanguage('cpp', highlightJsCpp);
+highlightJs.registerLanguage('sql', highlightJsSql);
+highlightJs.registerLanguage('go', highlightJsGo);
+highlightJs.registerLanguage('ruby', highlightJsRuby);
+highlightJs.registerLanguage('java', highlightJsJava);
+highlightJs.registerLanguage('ini', highlightJsIni);
diff --git a/web/react/utils/markdown.jsx b/web/react/utils/markdown.jsx
index 3ef09211f..374caf6dc 100644
--- a/web/react/utils/markdown.jsx
+++ b/web/react/utils/markdown.jsx
@@ -1,38 +1,14 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
+require('./highlight.jsx');
const TextFormatting = require('./text_formatting.jsx');
const Utils = require('./utils.jsx');
+const highlightJs = require('highlight.js/lib/highlight.js');
const marked = require('marked');
-const highlightJs = require('highlight.js/lib/highlight.js');
-const highlightJsDiff = require('highlight.js/lib/languages/diff.js');
-const highlightJsApache = require('highlight.js/lib/languages/apache.js');
-const highlightJsMakefile = require('highlight.js/lib/languages/makefile.js');
-const highlightJsHttp = require('highlight.js/lib/languages/http.js');
-const highlightJsJson = require('highlight.js/lib/languages/json.js');
-const highlightJsMarkdown = require('highlight.js/lib/languages/markdown.js');
-const highlightJsJavascript = require('highlight.js/lib/languages/javascript.js');
-const highlightJsCss = require('highlight.js/lib/languages/css.js');
-const highlightJsNginx = require('highlight.js/lib/languages/nginx.js');
-const highlightJsObjectivec = require('highlight.js/lib/languages/objectivec.js');
-const highlightJsPython = require('highlight.js/lib/languages/python.js');
-const highlightJsXml = require('highlight.js/lib/languages/xml.js');
-const highlightJsPerl = require('highlight.js/lib/languages/perl.js');
-const highlightJsBash = require('highlight.js/lib/languages/bash.js');
-const highlightJsPhp = require('highlight.js/lib/languages/php.js');
-const highlightJsCoffeescript = require('highlight.js/lib/languages/coffeescript.js');
-const highlightJsCs = require('highlight.js/lib/languages/cs.js');
-const highlightJsCpp = require('highlight.js/lib/languages/cpp.js');
-const highlightJsSql = require('highlight.js/lib/languages/sql.js');
-const highlightJsGo = require('highlight.js/lib/languages/go.js');
-const highlightJsRuby = require('highlight.js/lib/languages/ruby.js');
-const highlightJsJava = require('highlight.js/lib/languages/java.js');
-const highlightJsIni = require('highlight.js/lib/languages/ini.js');
-
-const Constants = require('../utils/constants.jsx');
-const HighlightedLanguages = Constants.HighlightedLanguages;
+const HighlightedLanguages = require('../utils/constants.jsx').HighlightedLanguages;
function markdownImageLoaded(image) {
image.style.height = 'auto';
@@ -84,30 +60,6 @@ class MattermostMarkdownRenderer extends marked.Renderer {
this.text = this.text.bind(this);
this.formattingOptions = formattingOptions;
-
- highlightJs.registerLanguage('diff', highlightJsDiff);
- highlightJs.registerLanguage('apache', highlightJsApache);
- highlightJs.registerLanguage('makefile', highlightJsMakefile);
- highlightJs.registerLanguage('http', highlightJsHttp);
- highlightJs.registerLanguage('json', highlightJsJson);
- highlightJs.registerLanguage('markdown', highlightJsMarkdown);
- highlightJs.registerLanguage('javascript', highlightJsJavascript);
- highlightJs.registerLanguage('css', highlightJsCss);
- highlightJs.registerLanguage('nginx', highlightJsNginx);
- highlightJs.registerLanguage('objectivec', highlightJsObjectivec);
- highlightJs.registerLanguage('python', highlightJsPython);
- highlightJs.registerLanguage('xml', highlightJsXml);
- highlightJs.registerLanguage('perl', highlightJsPerl);
- highlightJs.registerLanguage('bash', highlightJsBash);
- highlightJs.registerLanguage('php', highlightJsPhp);
- highlightJs.registerLanguage('coffeescript', highlightJsCoffeescript);
- highlightJs.registerLanguage('cs', highlightJsCs);
- highlightJs.registerLanguage('cpp', highlightJsCpp);
- highlightJs.registerLanguage('sql', highlightJsSql);
- highlightJs.registerLanguage('go', highlightJsGo);
- highlightJs.registerLanguage('ruby', highlightJsRuby);
- highlightJs.registerLanguage('java', highlightJsJava);
- highlightJs.registerLanguage('ini', highlightJsIni);
}
code(code, language) {
@@ -204,6 +156,301 @@ class MattermostMarkdownRenderer extends marked.Renderer {
}
}
+class MattermostLexer extends marked.Lexer {
+ token(originalSrc, top, bq) {
+ let src = originalSrc.replace(/^ +$/gm, '');
+
+ while (src) {
+ // newline
+ let cap = this.rules.newline.exec(src);
+ if (cap) {
+ src = src.substring(cap[0].length);
+ if (cap[0].length > 1) {
+ this.tokens.push({
+ type: 'space'
+ });
+ }
+ }
+
+ // code
+ cap = this.rules.code.exec(src);
+ if (cap) {
+ src = src.substring(cap[0].length);
+ cap = cap[0].replace(/^ {4}/gm, '');
+ this.tokens.push({
+ type: 'code',
+ text: this.options.pedantic ? cap : cap.replace(/\n+$/, '')
+ });
+ continue;
+ }
+
+ // fences (gfm)
+ cap = this.rules.fences.exec(src);
+ if (cap) {
+ src = src.substring(cap[0].length);
+ this.tokens.push({
+ type: 'code',
+ lang: cap[2],
+ text: cap[3] || ''
+ });
+ continue;
+ }
+
+ // heading
+ cap = this.rules.heading.exec(src);
+ if (cap) {
+ src = src.substring(cap[0].length);
+ this.tokens.push({
+ type: 'heading',
+ depth: cap[1].length,
+ text: cap[2]
+ });
+ continue;
+ }
+
+ // table no leading pipe (gfm)
+ cap = this.rules.nptable.exec(src);
+ if (top && cap) {
+ src = src.substring(cap[0].length);
+
+ const item = {
+ type: 'table',
+ header: cap[1].replace(/^ *| *\| *$/g, '').split(/ *\| */),
+ align: cap[2].replace(/^ *|\| *$/g, '').split(/ *\| */),
+ cells: cap[3].replace(/\n$/, '').split('\n')
+ };
+
+ for (let i = 0; i < item.align.length; i++) {
+ if (/^ *-+: *$/.test(item.align[i])) {
+ item.align[i] = 'right';
+ } else if (/^ *:-+: *$/.test(item.align[i])) {
+ item.align[i] = 'center';
+ } else if (/^ *:-+ *$/.test(item.align[i])) {
+ item.align[i] = 'left';
+ } else {
+ item.align[i] = null;
+ }
+ }
+
+ for (let i = 0; i < item.cells.length; i++) {
+ item.cells[i] = item.cells[i].split(/ *\| */);
+ }
+
+ this.tokens.push(item);
+
+ continue;
+ }
+
+ // lheading
+ cap = this.rules.lheading.exec(src);
+ if (cap) {
+ src = src.substring(cap[0].length);
+ this.tokens.push({
+ type: 'heading',
+ depth: cap[2] === '=' ? 1 : 2,
+ text: cap[1]
+ });
+ continue;
+ }
+
+ // hr
+ cap = this.rules.hr.exec(src);
+ if (cap) {
+ src = src.substring(cap[0].length);
+ this.tokens.push({
+ type: 'hr'
+ });
+ continue;
+ }
+
+ // blockquote
+ cap = this.rules.blockquote.exec(src);
+ if (cap) {
+ src = src.substring(cap[0].length);
+
+ this.tokens.push({
+ type: 'blockquote_start'
+ });
+
+ cap = cap[0].replace(/^ *> ?/gm, '');
+
+ // Pass `top` to keep the current
+ // "toplevel" state. This is exactly
+ // how markdown.pl works.
+ this.token(cap, top, true);
+
+ this.tokens.push({
+ type: 'blockquote_end'
+ });
+
+ continue;
+ }
+
+ // list
+ cap = this.rules.list.exec(src);
+ if (cap) {
+ const bull = cap[2];
+ let l = cap[0].length;
+
+ // Get each top-level item.
+ cap = cap[0].match(this.rules.item);
+
+ if (cap.length > 1) {
+ src = src.substring(l);
+
+ this.tokens.push({
+ type: 'list_start',
+ ordered: bull.length > 1
+ });
+
+ let next = false;
+ l = cap.length;
+
+ for (let i = 0; i < l; i++) {
+ let item = cap[i];
+
+ // Remove the list item's bullet
+ // so it is seen as the next token.
+ let space = item.length;
+ item = item.replace(/^ *([*+-]|\d+\.) +/, '');
+
+ // Outdent whatever the
+ // list item contains. Hacky.
+ if (~item.indexOf('\n ')) {
+ space -= item.length;
+ item = this.options.pedantic ? item.replace(/^ {1,4}/gm, '') : item.replace(new RegExp('^ \{1,' + space + '\}', 'gm'), '');
+ }
+
+ // Determine whether the next list item belongs here.
+ // Backpedal if it does not belong in this list.
+ if (this.options.smartLists && i !== l - 1) {
+ const bullet = /(?:[*+-]|\d+\.)/;
+ const b = bullet.exec(cap[i + 1])[0];
+ if (bull !== b && !(bull.length > 1 && b.length > 1)) {
+ src = cap.slice(i + 1).join('\n') + src;
+ i = l - 1;
+ }
+ }
+
+ // Determine whether item is loose or not.
+ // Use: /(^|\n)(?! )[^\n]+\n\n(?!\s*$)/
+ // for discount behavior.
+ let loose = next || (/\n\n(?!\s*$)/).test(item);
+ if (i !== l - 1) {
+ next = item.charAt(item.length - 1) === '\n';
+ if (!loose) {
+ loose = next;
+ }
+ }
+
+ this.tokens.push({
+ type: loose ? 'loose_item_start' : 'list_item_start'
+ });
+
+ // Recurse.
+ this.token(item, false, bq);
+
+ this.tokens.push({
+ type: 'list_item_end'
+ });
+ }
+
+ this.tokens.push({
+ type: 'list_end'
+ });
+
+ continue;
+ }
+ }
+
+ // html
+ cap = this.rules.html.exec(src);
+ if (cap) {
+ src = src.substring(cap[0].length);
+ this.tokens.push({
+ type: this.options.sanitize ? 'paragraph' : 'html',
+ pre: !this.options.sanitizer && (cap[1] === 'pre' || cap[1] === 'script' || cap[1] === 'style'),
+ text: cap[0]
+ });
+ continue;
+ }
+
+ // def
+ cap = this.rules.def.exec(src);
+ if ((!bq && top) && cap) {
+ src = src.substring(cap[0].length);
+ this.tokens.links[cap[1].toLowerCase()] = {
+ href: cap[2],
+ title: cap[3]
+ };
+ continue;
+ }
+
+ // table (gfm)
+ cap = this.rules.table.exec(src);
+ if (top && cap) {
+ src = src.substring(cap[0].length);
+
+ const item = {
+ type: 'table',
+ header: cap[1].replace(/^ *| *\| *$/g, '').split(/ *\| */),
+ align: cap[2].replace(/^ *|\| *$/g, '').split(/ *\| */),
+ cells: cap[3].replace(/(?: *\| *)?\n$/, '').split('\n')
+ };
+
+ for (let i = 0; i < item.align.length; i++) {
+ if (/^ *-+: *$/.test(item.align[i])) {
+ item.align[i] = 'right';
+ } else if (/^ *:-+: *$/.test(item.align[i])) {
+ item.align[i] = 'center';
+ } else if (/^ *:-+ *$/.test(item.align[i])) {
+ item.align[i] = 'left';
+ } else {
+ item.align[i] = null;
+ }
+ }
+
+ for (let i = 0; i < item.cells.length; i++) {
+ item.cells[i] = item.cells[i].replace(/^ *\| *| *\| *$/g, '').split(/ *\| */);
+ }
+
+ this.tokens.push(item);
+
+ continue;
+ }
+
+ // top-level paragraph
+ cap = this.rules.paragraph.exec(src);
+ if (top && cap) {
+ src = src.substring(cap[0].length);
+ this.tokens.push({
+ type: 'paragraph',
+ text: cap[1].charAt(cap[1].length - 1) === '\n' ? cap[1].slice(0, -1) : cap[1]
+ });
+ continue;
+ }
+
+ // text
+ cap = this.rules.text.exec(src);
+ if (cap) {
+ // Top-level should never reach here.
+ src = src.substring(cap[0].length);
+ this.tokens.push({
+ type: 'text',
+ text: cap[0]
+ });
+ continue;
+ }
+
+ if (src) {
+ throw new Error('Infinite loop on byte: ' + src.charCodeAt(0));
+ }
+ }
+
+ return this.tokens;
+ }
+}
+
export function format(text, options) {
const markdownOptions = {
renderer: new MattermostMarkdownRenderer(null, options),
@@ -212,7 +459,7 @@ export function format(text, options) {
tables: true
};
- const tokens = marked.lexer(text, markdownOptions);
+ const tokens = new MattermostLexer(markdownOptions).lex(text);
return new MattermostParser(markdownOptions).parse(tokens);
}
diff --git a/web/react/utils/utils.jsx b/web/react/utils/utils.jsx
index f006956bc..8052c000c 100644
--- a/web/react/utils/utils.jsx
+++ b/web/react/utils/utils.jsx
@@ -1132,3 +1132,12 @@ export function sortByDisplayName(a, b) {
}
return 0;
}
+
+export function getChannelTerm(channelType) {
+ let channelTerm = 'Channel';
+ if (channelType === Constants.PRIVATE_CHANNEL) {
+ channelTerm = 'Group';
+ }
+
+ return channelTerm;
+}
diff --git a/web/web.go b/web/web.go
index 1b2f5f742..1cae604ae 100644
--- a/web/web.go
+++ b/web/web.go
@@ -983,6 +983,11 @@ func incomingWebhook(c *api.Context, w http.ResponseWriter, r *http.Request) {
parsedRequest = model.IncomingWebhookRequestFromJson(strings.NewReader(r.FormValue("payload")))
}
+ if parsedRequest == nil {
+ c.Err = model.NewAppError("incomingWebhook", "Unable to parse incoming data", "")
+ return
+ }
+
text := parsedRequest.Text
if len(text) == 0 {
c.Err = model.NewAppError("incomingWebhook", "No text specified", "")