summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--api/admin_test.go8
-rw-r--r--api/user.go1
-rw-r--r--doc/developer/tests/test-emoticons.md929
-rw-r--r--doc/developer/tests/test-links.md72
-rw-r--r--doc/help/Team-Settings.md3
-rw-r--r--doc/install/Production-Ubuntu.md6
-rw-r--r--model/preference.go1
-rw-r--r--store/sql_post_store.go17
-rw-r--r--store/sql_post_store_test.go5
-rw-r--r--store/sql_preference_store.go47
-rw-r--r--store/sql_preference_store_test.go135
-rw-r--r--store/sql_store.go2
-rw-r--r--store/store.go1
-rw-r--r--utils/time.go23
-rw-r--r--utils/time_test.go50
-rw-r--r--web/react/components/delete_post_modal.jsx1
-rw-r--r--web/react/components/navbar.jsx2
-rw-r--r--web/react/components/textbox.jsx23
-rw-r--r--web/react/components/user_settings/user_settings_advanced.jsx144
-rw-r--r--web/react/stores/post_store.jsx14
-rw-r--r--web/react/utils/constants.jsx7
-rw-r--r--web/react/utils/markdown.jsx114
-rw-r--r--web/react/utils/utils.jsx4
-rw-r--r--web/sass-files/sass/partials/_base.scss2
-rw-r--r--web/sass-files/sass/partials/_post.scss12
25 files changed, 598 insertions, 1025 deletions
diff --git a/api/admin_test.go b/api/admin_test.go
index 0db5caa4c..0a1682a99 100644
--- a/api/admin_test.go
+++ b/api/admin_test.go
@@ -235,6 +235,10 @@ func TestGetPostCount(t *testing.T) {
post1 := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a"}
post1 = Client.Must(Client.CreatePost(post1)).Data.(*model.Post)
+ // manually update creation time, since it's always set to 0 upon saving and we only retrieve posts < today
+ Srv.Store.(*store.SqlStore).GetMaster().Exec("UPDATE Posts SET CreateAt = :CreateAt WHERE ChannelId = :ChannelId",
+ map[string]interface{}{"ChannelId": channel1.Id, "CreateAt": utils.MillisFromTime(utils.Yesterday())})
+
if _, err := Client.GetAnalytics(team.Id, "post_counts_day"); err == nil {
t.Fatal("Shouldn't have permissions")
}
@@ -276,6 +280,10 @@ func TestUserCountsWithPostsByDay(t *testing.T) {
post1 := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a"}
post1 = Client.Must(Client.CreatePost(post1)).Data.(*model.Post)
+ // manually update creation time, since it's always set to 0 upon saving and we only retrieve posts < today
+ Srv.Store.(*store.SqlStore).GetMaster().Exec("UPDATE Posts SET CreateAt = :CreateAt WHERE ChannelId = :ChannelId",
+ map[string]interface{}{"ChannelId": channel1.Id, "CreateAt": utils.MillisFromTime(utils.Yesterday())})
+
if _, err := Client.GetAnalytics(team.Id, "user_counts_with_posts_day"); err == nil {
t.Fatal("Shouldn't have permissions")
}
diff --git a/api/user.go b/api/user.go
index 3281e83e2..62947d8fd 100644
--- a/api/user.go
+++ b/api/user.go
@@ -334,6 +334,7 @@ func LoginByEmail(c *Context, w http.ResponseWriter, r *http.Request, email, nam
if result := <-Srv.Store.User().GetByEmail(team.Id, email); result.Err != nil {
c.Err = result.Err
+ c.Err.StatusCode = http.StatusForbidden
return nil
} else {
user := result.Data.(*model.User)
diff --git a/doc/developer/tests/test-emoticons.md b/doc/developer/tests/test-emoticons.md
index 5b1a7c7d8..6338d7333 100644
--- a/doc/developer/tests/test-emoticons.md
+++ b/doc/developer/tests/test-emoticons.md
@@ -1,923 +1,22 @@
-# Emoticon Testing
+# Emoticon Testing
+Verify that all emoticons render. This test should render in three separate messages since it's ~11000 characters.
-Verify that all emoticons render. This test should render in three seperate messages since it's ~11000 characters.
+### Emoticon - Punctuation
-### Emoticon - Punctuation
+:) :-) ;) ;-) :o :O :-o :-O :] :-] :d :-D x-d x-D :p :-P :@ :( :-( :'( :* :-* :/ :-/ :s :-s :| :-| :$ :-$ :-x <3 :+1: :-1:
-:) :-)
-;) ;-)
-: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 - People
+### 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:
-: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 - 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 - 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:
+### 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/doc/developer/tests/test-links.md b/doc/developer/tests/test-links.md
index 62b729b30..011542c82 100644
--- a/doc/developer/tests/test-links.md
+++ b/doc/developer/tests/test-links.md
@@ -1,16 +1,68 @@
-
# Link Testing
+
+Links in Mattermosts should render as specified below.
-Links in Mattermosts should render as specified below. Paste the below text into Mattermost to test text processing.
+#### These strings should auto-link:
+
+http://example.com
+https://example.com
+www.example.com
+www.example.com/index
+www.example.com/index.html
+www.example.com/index/sub
+www.example.com/index?params=1
+www.example.com/index?params=1&other=2
+www.example.com/index?params=1;other=2
+http://example.com:8065
+http://www.example.com/_/page
+www.example.com/_/page
+https://en.wikipedia.org/wiki/🐬
+https://en.wikipedia.org/wiki/Rendering_(computer_graphics)
+http://127.0.0.1
+http://192.168.1.1:4040
+http://[::1]:80
+http://[::1]:8065
+https://[::1]:80
+http://[2001:0:5ef5:79fb:303a:62d5:3312:ff42]:80
+http://[2001:0:5ef5:79fb:303a:62d5:3312:ff42]:8065
+https://[2001:0:5ef5:79fb:303a:62d5:3312:ff42]:443
+http://username:password@example.com
+http://username:password@127.0.0.1
+http://username:password@[2001:0:5ef5:79fb:303a:62d5:3312:ff42]:80
+test@example.com
+
+#### These strings should not auto-link:
+
+example.com
+readme.md
+http://
+@example.com
+./make-compiled-client.sh
+test.:test
-```
-These strings should auto-link:
+#### Only the links within these sentences should auto-link:
-http://wikipedia.com
-https://wikipedia.com
-www.wikipedia.com
+(http://example.com)
+(test@example.com)
+This is a sentence with a http://example.com in it.
+This is a sentence with a [link](http://example.com) in it.
+This is a sentence with a http://example.com/_/underscore in it.
+This is a sentence with a link (http://example.com) in it.
+This is a sentence with a (https://en.wikipedia.org/wiki/Rendering_(computer_graphics)) in it.
+This is a sentence with a http://192.168.1.1:4040 in it.
+This is a sentence with a https://[::1]:80 in it.
+This is a link to http://example.com.
-These strings should not auto-link:
+#### These links should auto-link to the specified location:
-Readme.md
-```
+[example link](example.com) links to `http://example.com`
+[example.com](example.com) links to `http://example.com`
+[example.com/other](example.com) links to `http://example.com`
+[example.com/other_link](example.com/example) links to `http://example.com/example`
+www.example.com links to `http://www.example.com`
+https://example.com links to `https://example.com` and not `http://example.com`
+https://en.wikipedia.org/wiki/🐬 links to the Wikipedia article on dolphins
+https://en.wikipedia.org/wiki/URLs#Syntax links to the Syntax section of the Wikipedia article on URLs
+test@example.com links to `mailto:test@example.com`
+[email link](mailto:test@example.com) links to `mailto:test@example.com` and not `http://mailto:test@example.com`
+[other link](ts3server://example.com) links to `ts3server://example.com` and not `http://ts3server://example.com`
diff --git a/doc/help/Team-Settings.md b/doc/help/Team-Settings.md
index 7c8665565..fead9f4ca 100644
--- a/doc/help/Team-Settings.md
+++ b/doc/help/Team-Settings.md
@@ -64,7 +64,8 @@ The Slack Import feature in Mattermost is in "Beta" and focus is on supporting m
#### Notes:
-- Newly added markdown suppport in Slack's Posts 2.0 feature announced on September 28, 2015 is not yet supported.
+- Users are not automatically added to channels or groups when importing from Slack.
+- Newly added markdown support in Slack's Posts 2.0 feature announced on September 28, 2015 is not yet supported.
- Slack does not export files or images your team has stored in Slack's database. Mattermost will provide links to the location of your assets in Slack's web UI.
- Slack does not export any content from private groups or direct messages that your team has stored in Slack's database.
- In Beta, Slack accounts with usernames or email addresses identical to existing Mattermost accounts will not import and mentions do not resolve as Mattermost usernames (still shows Slack ID). No pre-check or roll-back is currently offered.
diff --git a/doc/install/Production-Ubuntu.md b/doc/install/Production-Ubuntu.md
index 482c2a0ba..d6b98981c 100644
--- a/doc/install/Production-Ubuntu.md
+++ b/doc/install/Production-Ubuntu.md
@@ -37,18 +37,18 @@
## Set up Mattermost Server
1. For the purposes of this guide we will assume this server has an IP address of 10.10.10.2
+1. For the sake of making this guide simple we located the files at `/home/ubuntu/mattermost`. In the future we will give guidance for storing under `/opt`.
+1. We have also elected to run the Mattermost Server as the `ubuntu` account for simplicity. We recommend setting up and running the service under a `mattermost` user account with limited permissions.
1. Download the latest Mattermost Server by typing:
* ``` wget https://github.com/mattermost/platform/releases/download/v1.2.1/mattermost.tar.gz```
1. Unzip the Mattermost Server by typing:
* ``` tar -xvzf mattermost.tar.gz```
-1. For the sake of making this guide simple we located the files at `/home/ubuntu/mattermost`. In the future we will give guidance for storing under `/opt`.
-1. We have also elected to run the Mattermost Server as the `ubuntu` account for simplicity. We recommend setting up and running the service under a `mattermost` user account with limited permissions.
1. Create the storage directory for files. We assume you will have attached a large drive for storage of images and files. For this setup we will assume the directory is located at `/mattermost/data`.
* Create the directory by typing:
* ``` sudo mkdir -p /mattermost/data```
* Set the ubuntu account as the directory owner by typing:
* ``` sudo chown -R ubuntu /mattermost```
-1. Configure Mattermost Server by editing the config.json file at /home/ubuntu/mattermost/config`
+1. Configure Mattermost Server by editing the config.json file at `/home/ubuntu/mattermost/config`
* ``` cd ~/mattermost/config```
* Edit the file by typing:
* ``` vi config.json```
diff --git a/model/preference.go b/model/preference.go
index 892ae82aa..4f2ba0099 100644
--- a/model/preference.go
+++ b/model/preference.go
@@ -12,6 +12,7 @@ import (
const (
PREFERENCE_CATEGORY_DIRECT_CHANNEL_SHOW = "direct_channel_show"
PREFERENCE_CATEGORY_TUTORIAL_STEPS = "tutorial_step"
+ PREFERENCE_CATEGORY_ADVANCED_SETTINGS = "advanced_settings"
)
type Preference struct {
diff --git a/store/sql_post_store.go b/store/sql_post_store.go
index cc596074f..1831eb23c 100644
--- a/store/sql_post_store.go
+++ b/store/sql_post_store.go
@@ -807,6 +807,7 @@ func (s SqlPostStore) AnalyticsUserCountsWithPostsByDay(teamId string) StoreChan
WHERE
Posts.ChannelId = Channels.Id
AND Channels.TeamId = :TeamId
+ AND Posts.CreateAt <= :EndTime
ORDER BY Name DESC) AS t1
GROUP BY Name
ORDER BY Name DESC
@@ -825,17 +826,20 @@ func (s SqlPostStore) AnalyticsUserCountsWithPostsByDay(teamId string) StoreChan
WHERE
Posts.ChannelId = Channels.Id
AND Channels.TeamId = :TeamId
+ AND Posts.CreateAt <= :EndTime
ORDER BY Name DESC) AS t1
GROUP BY Name
ORDER BY Name DESC
LIMIT 30`
}
+ end := utils.MillisFromTime(utils.EndOfDay(utils.Yesterday()))
+
var rows model.AnalyticsRows
_, err := s.GetReplica().Select(
&rows,
query,
- map[string]interface{}{"TeamId": teamId, "Time": model.GetMillis() - 1000*60*60*24*31})
+ map[string]interface{}{"TeamId": teamId, "EndTime": end})
if err != nil {
result.Err = model.NewAppError("SqlPostStore.AnalyticsUserCountsWithPostsByDay", "We couldn't get user counts with posts", err.Error())
} else {
@@ -867,7 +871,8 @@ func (s SqlPostStore) AnalyticsPostCountsByDay(teamId string) StoreChannel {
WHERE
Posts.ChannelId = Channels.Id
AND Channels.TeamId = :TeamId
- AND Posts.CreateAt >:Time) AS t1
+ AND Posts.CreateAt <= :EndTime
+ AND Posts.CreateAt >= :StartTime) AS t1
GROUP BY Name
ORDER BY Name DESC
LIMIT 30`
@@ -885,17 +890,21 @@ func (s SqlPostStore) AnalyticsPostCountsByDay(teamId string) StoreChannel {
WHERE
Posts.ChannelId = Channels.Id
AND Channels.TeamId = :TeamId
- AND Posts.CreateAt > :Time) AS t1
+ AND Posts.CreateAt <= :EndTime
+ AND Posts.CreateAt >= :StartTime) AS t1
GROUP BY Name
ORDER BY Name DESC
LIMIT 30`
}
+ end := utils.MillisFromTime(utils.EndOfDay(utils.Yesterday()))
+ start := utils.MillisFromTime(utils.StartOfDay(utils.Yesterday().AddDate(0, 0, -31)))
+
var rows model.AnalyticsRows
_, err := s.GetReplica().Select(
&rows,
query,
- map[string]interface{}{"TeamId": teamId, "Time": model.GetMillis() - 1000*60*60*24*31})
+ map[string]interface{}{"TeamId": teamId, "StartTime": start, "EndTime": end})
if err != nil {
result.Err = model.NewAppError("SqlPostStore.AnalyticsPostCountsByDay", "We couldn't get post counts by day", err.Error())
} else {
diff --git a/store/sql_post_store_test.go b/store/sql_post_store_test.go
index d9b087ea7..12b50cad3 100644
--- a/store/sql_post_store_test.go
+++ b/store/sql_post_store_test.go
@@ -9,6 +9,7 @@ import (
"time"
"github.com/mattermost/platform/model"
+ "github.com/mattermost/platform/utils"
)
func TestPostStoreSave(t *testing.T) {
@@ -776,7 +777,7 @@ func TestUserCountsWithPostsByDay(t *testing.T) {
o1 := &model.Post{}
o1.ChannelId = c1.Id
o1.UserId = model.NewId()
- o1.CreateAt = model.GetMillis()
+ o1.CreateAt = utils.MillisFromTime(utils.Yesterday())
o1.Message = "a" + model.NewId() + "b"
o1 = Must(store.Post().Save(o1)).(*model.Post)
@@ -836,7 +837,7 @@ func TestPostCountsByDay(t *testing.T) {
o1 := &model.Post{}
o1.ChannelId = c1.Id
o1.UserId = model.NewId()
- o1.CreateAt = model.GetMillis()
+ o1.CreateAt = utils.MillisFromTime(utils.Yesterday())
o1.Message = "a" + model.NewId() + "b"
o1 = Must(store.Post().Save(o1)).(*model.Post)
diff --git a/store/sql_preference_store.go b/store/sql_preference_store.go
index 8454abcbd..f73dad3ac 100644
--- a/store/sql_preference_store.go
+++ b/store/sql_preference_store.go
@@ -4,6 +4,7 @@
package store
import (
+ l4g "code.google.com/p/log4go"
"github.com/go-gorp/gorp"
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/utils"
@@ -13,6 +14,10 @@ type SqlPreferenceStore struct {
*SqlStore
}
+const (
+ FEATURE_TOGGLE_PREFIX = "feature_enabled_"
+)
+
func NewSqlPreferenceStore(sqlStore *SqlStore) PreferenceStore {
s := &SqlPreferenceStore{sqlStore}
@@ -36,6 +41,23 @@ func (s SqlPreferenceStore) CreateIndexesIfNotExists() {
s.CreateIndexIfNotExists("idx_preferences_name", "Preferences", "Name")
}
+func (s SqlPreferenceStore) DeleteUnusedFeatures() {
+ l4g.Debug("Deleting any unused pre-release features")
+
+ sql := `DELETE
+ FROM Preferences
+ WHERE
+ Category = :Category
+ AND Value = :Value
+ AND Name LIKE '` + FEATURE_TOGGLE_PREFIX + `%'`
+
+ queryParams := map[string]string{
+ "Category": model.PREFERENCE_CATEGORY_ADVANCED_SETTINGS,
+ "Value": "false",
+ }
+ s.GetMaster().Exec(sql, queryParams)
+}
+
func (s SqlPreferenceStore) Save(preferences *model.Preferences) StoreChannel {
storeChannel := make(StoreChannel)
@@ -257,3 +279,28 @@ func (s SqlPreferenceStore) PermanentDeleteByUser(userId string) StoreChannel {
return storeChannel
}
+
+func (s SqlPreferenceStore) IsFeatureEnabled(feature, userId string) StoreChannel {
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+ if value, err := s.GetReplica().SelectStr(`SELECT
+ value
+ FROM
+ Preferences
+ WHERE
+ UserId = :UserId
+ AND Category = :Category
+ AND Name = :Name`, map[string]interface{}{"UserId": userId, "Category": model.PREFERENCE_CATEGORY_ADVANCED_SETTINGS, "Name": FEATURE_TOGGLE_PREFIX + feature}); err != nil {
+ result.Err = model.NewAppError("SqlPreferenceStore.IsFeatureEnabled", "We encountered an error while finding a pre release feature preference", err.Error())
+ } else {
+ result.Data = value == "true"
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
diff --git a/store/sql_preference_store_test.go b/store/sql_preference_store_test.go
index 77da71fd6..6f8f44f47 100644
--- a/store/sql_preference_store_test.go
+++ b/store/sql_preference_store_test.go
@@ -232,3 +232,138 @@ func TestPreferenceDelete(t *testing.T) {
t.Fatal(result.Err)
}
}
+
+func TestIsFeatureEnabled(t *testing.T) {
+ Setup()
+
+ feature1 := "testFeat1"
+ feature2 := "testFeat2"
+ feature3 := "testFeat3"
+
+ userId := model.NewId()
+ category := model.PREFERENCE_CATEGORY_ADVANCED_SETTINGS
+
+ features := model.Preferences{
+ {
+ UserId: userId,
+ Category: category,
+ Name: FEATURE_TOGGLE_PREFIX + feature1,
+ Value: "true",
+ },
+ {
+ UserId: userId,
+ Category: category,
+ Name: model.NewId(),
+ Value: "false",
+ },
+ {
+ UserId: userId,
+ Category: model.NewId(),
+ Name: FEATURE_TOGGLE_PREFIX + feature1,
+ Value: "false",
+ },
+ {
+ UserId: model.NewId(),
+ Category: category,
+ Name: FEATURE_TOGGLE_PREFIX + feature2,
+ Value: "false",
+ },
+ {
+ UserId: model.NewId(),
+ Category: category,
+ Name: FEATURE_TOGGLE_PREFIX + feature3,
+ Value: "foobar",
+ },
+ }
+
+ Must(store.Preference().Save(&features))
+
+ if result := <-store.Preference().IsFeatureEnabled(feature1, userId); result.Err != nil {
+ t.Fatal(result.Err)
+ } else if data := result.Data.(bool); data != true {
+ t.Fatalf("got incorrect setting for feature1, %v=%v", true, data)
+ }
+
+ if result := <-store.Preference().IsFeatureEnabled(feature2, userId); result.Err != nil {
+ t.Fatal(result.Err)
+ } else if data := result.Data.(bool); data != false {
+ t.Fatalf("got incorrect setting for feature2, %v=%v", false, data)
+ }
+
+ // make sure we get false if something different than "true" or "false" has been saved to database
+ if result := <-store.Preference().IsFeatureEnabled(feature3, userId); result.Err != nil {
+ t.Fatal(result.Err)
+ } else if data := result.Data.(bool); data != false {
+ t.Fatalf("got incorrect setting for feature3, %v=%v", false, data)
+ }
+
+ // make sure false is returned if a non-existent feature is queried
+ if result := <-store.Preference().IsFeatureEnabled("someOtherFeature", userId); result.Err != nil {
+ t.Fatal(result.Err)
+ } else if data := result.Data.(bool); data != false {
+ t.Fatalf("got incorrect setting for non-existent feature 'someOtherFeature', %v=%v", false, data)
+ }
+}
+
+func TestDeleteUnusedFeatures(t *testing.T) {
+ Setup()
+
+ userId1 := model.NewId()
+ userId2 := model.NewId()
+ category := model.PREFERENCE_CATEGORY_ADVANCED_SETTINGS
+ feature1 := "feature1"
+ feature2 := "feature2"
+
+ features := model.Preferences{
+ {
+ UserId: userId1,
+ Category: category,
+ Name: FEATURE_TOGGLE_PREFIX + feature1,
+ Value: "true",
+ },
+ {
+ UserId: userId2,
+ Category: category,
+ Name: FEATURE_TOGGLE_PREFIX + feature1,
+ Value: "false",
+ },
+ {
+ UserId: userId1,
+ Category: category,
+ Name: FEATURE_TOGGLE_PREFIX + feature2,
+ Value: "false",
+ },
+ {
+ UserId: userId2,
+ Category: category,
+ Name: FEATURE_TOGGLE_PREFIX + feature2,
+ Value: "true",
+ },
+ }
+
+ Must(store.Preference().Save(&features))
+
+ store.(*SqlStore).preference.(*SqlPreferenceStore).DeleteUnusedFeatures()
+
+ //make sure features with value "false" have actually been deleted from the database
+ if val, err := store.(*SqlStore).preference.(*SqlPreferenceStore).GetReplica().SelectInt(`SELECT COUNT(*)
+ FROM Preferences
+ WHERE Category = :Category
+ AND Value = :Val
+ AND Name LIKE '`+FEATURE_TOGGLE_PREFIX+`%'`, map[string]interface{}{"Category": model.PREFERENCE_CATEGORY_ADVANCED_SETTINGS, "Val": "false"}); err != nil {
+ t.Fatal(err)
+ } else if val != 0 {
+ t.Fatalf("Found %d features with value 'false', expected all to be deleted", val)
+ }
+ //
+ // make sure features with value "true" remain saved
+ if val, err := store.(*SqlStore).preference.(*SqlPreferenceStore).GetReplica().SelectInt(`SELECT COUNT(*)
+ FROM Preferences
+ WHERE Category = :Category
+ AND Value = :Val
+ AND Name LIKE '`+FEATURE_TOGGLE_PREFIX+`%'`, map[string]interface{}{"Category": model.PREFERENCE_CATEGORY_ADVANCED_SETTINGS, "Val": "true"}); err != nil {
+ t.Fatal(err)
+ } else if val == 0 {
+ t.Fatalf("Found %d features with value 'true', expected to find at least %d features", val, 2)
+ }
+}
diff --git a/store/sql_store.go b/store/sql_store.go
index f348db10b..d17a3e8c3 100644
--- a/store/sql_store.go
+++ b/store/sql_store.go
@@ -148,6 +148,8 @@ func NewSqlStore() Store {
sqlStore.webhook.(*SqlWebhookStore).CreateIndexesIfNotExists()
sqlStore.preference.(*SqlPreferenceStore).CreateIndexesIfNotExists()
+ sqlStore.preference.(*SqlPreferenceStore).DeleteUnusedFeatures()
+
if model.IsPreviousVersion(schemaVersion) || isSchemaVersion07 || isSchemaVersion10 {
sqlStore.system.Update(&model.System{Name: "Version", Value: model.CurrentVersion})
l4g.Warn("The database schema has been upgraded to version " + model.CurrentVersion)
diff --git a/store/store.go b/store/store.go
index 338ae186f..0695ea27f 100644
--- a/store/store.go
+++ b/store/store.go
@@ -186,4 +186,5 @@ type PreferenceStore interface {
GetCategory(userId string, category string) StoreChannel
GetAll(userId string) StoreChannel
PermanentDeleteByUser(userId string) StoreChannel
+ IsFeatureEnabled(feature, userId string) StoreChannel
}
diff --git a/utils/time.go b/utils/time.go
new file mode 100644
index 000000000..7d5afdf8f
--- /dev/null
+++ b/utils/time.go
@@ -0,0 +1,23 @@
+package utils
+
+import (
+ "time"
+)
+
+func MillisFromTime(t time.Time) int64 {
+ return t.UnixNano() / int64(time.Millisecond)
+}
+
+func StartOfDay(t time.Time) time.Time {
+ year, month, day := t.Date()
+ return time.Date(year, month, day, 0, 0, 0, 0, t.Location())
+}
+
+func EndOfDay(t time.Time) time.Time {
+ year, month, day := t.Date()
+ return time.Date(year, month, day, 23, 59, 59, 999999999, t.Location())
+}
+
+func Yesterday() time.Time {
+ return time.Now().AddDate(0, 0, -1)
+}
diff --git a/utils/time_test.go b/utils/time_test.go
new file mode 100644
index 000000000..7d65046bf
--- /dev/null
+++ b/utils/time_test.go
@@ -0,0 +1,50 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package utils
+
+import (
+ "testing"
+ "time"
+)
+
+var format = "2006-01-02 15:04:05.000000000"
+
+func TestMillisFromTime(t *testing.T) {
+ input, _ := time.Parse(format, "2015-01-01 12:34:00.000000000")
+ actual := MillisFromTime(input)
+ expected := int64(1420115640000)
+
+ if actual != expected {
+ t.Fatalf("TestMillisFromTime failed, %v=%v", expected, actual)
+ }
+}
+
+func TestYesterday(t *testing.T) {
+ actual := Yesterday()
+ expected := time.Now().AddDate(0, 0, -1)
+
+ if actual.Year() != expected.Year() || actual.Day() != expected.Day() || actual.Month() != expected.Month() {
+ t.Fatalf("TestYesterday failed, %v=%v", expected, actual)
+ }
+}
+
+func TestStartOfDay(t *testing.T) {
+ input, _ := time.Parse(format, "2015-01-01 12:34:00.000000000")
+ actual := StartOfDay(input)
+ expected, _ := time.Parse(format, "2015-01-01 00:00:00.000000000")
+
+ if actual != expected {
+ t.Fatalf("TestStartOfDay failed, %v=%v", expected, actual)
+ }
+}
+
+func TestEndOfDay(t *testing.T) {
+ input, _ := time.Parse(format, "2015-01-01 12:34:00.000000000")
+ actual := EndOfDay(input)
+ expected, _ := time.Parse(format, "2015-01-01 23:59:59.999999999")
+
+ if actual != expected {
+ t.Fatalf("TestEndOfDay failed, %v=%v", expected, actual)
+ }
+}
diff --git a/web/react/components/delete_post_modal.jsx b/web/react/components/delete_post_modal.jsx
index 3c4b17905..5e89a0893 100644
--- a/web/react/components/delete_post_modal.jsx
+++ b/web/react/components/delete_post_modal.jsx
@@ -152,6 +152,7 @@ export default class DeletePostModal extends React.Component {
type='button'
className='btn btn-danger'
onClick={this.handleDelete}
+ autoFocus='autofocus'
>
{'Delete'}
</button>
diff --git a/web/react/components/navbar.jsx b/web/react/components/navbar.jsx
index 03cc75a08..6c3bfc7db 100644
--- a/web/react/components/navbar.jsx
+++ b/web/react/components/navbar.jsx
@@ -174,7 +174,7 @@ export default class Navbar extends React.Component {
<a
role='menuitem'
href='#'
- onClick={() => this.setState({showInviteModal: false})}
+ onClick={() => this.setState({showInviteModal: true})}
>
{'Add Members'}
</a>
diff --git a/web/react/components/textbox.jsx b/web/react/components/textbox.jsx
index e2868e946..10b3c0069 100644
--- a/web/react/components/textbox.jsx
+++ b/web/react/components/textbox.jsx
@@ -11,6 +11,7 @@ import * as Utils from '../utils/utils.jsx';
import Constants from '../utils/constants.jsx';
const ActionTypes = Constants.ActionTypes;
const KeyCodes = Constants.KeyCodes;
+const PreReleaseFeatures = Constants.PRE_RELEASE_FEATURES;
export default class Textbox extends React.Component {
constructor(props) {
@@ -303,7 +304,19 @@ export default class Textbox extends React.Component {
}
render() {
- const previewLinkVisible = this.props.messageText.length > 0;
+ let previewLink = null;
+ if (Utils.isFeatureEnabled(PreReleaseFeatures.MARKDOWN_PREVIEW)) {
+ const previewLinkVisible = this.props.messageText.length > 0;
+ previewLink = (
+ <a
+ style={{visibility: previewLinkVisible ? 'visible' : 'hidden'}}
+ onClick={this.showPreview}
+ className='textbox-preview-link'
+ >
+ {this.state.preview ? 'Edit message' : 'Preview'}
+ </a>
+ );
+ }
return (
<div
@@ -342,19 +355,13 @@ export default class Textbox extends React.Component {
dangerouslySetInnerHTML={{__html: this.state.preview ? TextFormatting.formatText(this.props.messageText) : ''}}
>
</div>
+ {previewLink}
<a
onClick={this.showHelp}
className='textbox-help-link'
>
{'Help'}
</a>
- <a
- style={{visibility: previewLinkVisible ? 'visible' : 'hidden'}}
- onClick={this.showPreview}
- className='textbox-preview-link'
- >
- {this.state.preview ? 'Edit' : 'Preview'}
- </a>
</div>
);
}
diff --git a/web/react/components/user_settings/user_settings_advanced.jsx b/web/react/components/user_settings/user_settings_advanced.jsx
index ac82595f5..b4d34c658 100644
--- a/web/react/components/user_settings/user_settings_advanced.jsx
+++ b/web/react/components/user_settings/user_settings_advanced.jsx
@@ -6,6 +6,7 @@ import SettingItemMin from '../setting_item_min.jsx';
import SettingItemMax from '../setting_item_max.jsx';
import Constants from '../../utils/constants.jsx';
import PreferenceStore from '../../stores/preference_store.jsx';
+const PreReleaseFeatures = Constants.PRE_RELEASE_FEATURES;
export default class AdvancedSettingsDisplay extends React.Component {
constructor(props) {
@@ -13,21 +14,33 @@ export default class AdvancedSettingsDisplay extends React.Component {
this.updateSection = this.updateSection.bind(this);
this.updateSetting = this.updateSetting.bind(this);
- this.setupInitialState = this.setupInitialState.bind(this);
+ this.toggleFeature = this.toggleFeature.bind(this);
+ this.saveEnabledFeatures = this.saveEnabledFeatures.bind(this);
- this.state = this.setupInitialState();
- }
+ const preReleaseFeaturesKeys = Object.keys(PreReleaseFeatures);
+ const advancedSettings = PreferenceStore.getPreferences(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS);
+ const settings = {
+ send_on_ctrl_enter: PreferenceStore.getPreference(
+ Constants.Preferences.CATEGORY_ADVANCED_SETTINGS,
+ 'send_on_ctrl_enter',
+ {value: 'false'}
+ ).value
+ };
- setupInitialState() {
- const sendOnCtrlEnter = PreferenceStore.getPreference(
- Constants.Preferences.CATEGORY_ADVANCED_SETTINGS,
- 'send_on_ctrl_enter',
- {value: 'false'}
- ).value;
+ let enabledFeatures = 0;
+ advancedSettings.forEach((setting) => {
+ preReleaseFeaturesKeys.forEach((key) => {
+ const feature = PreReleaseFeatures[key];
+ if (setting.name === Constants.FeatureTogglePrefix + feature.label) {
+ settings[setting.name] = setting.value;
+ if (setting.value === 'true') {
+ enabledFeatures++;
+ }
+ }
+ });
+ });
- return {
- settings: {send_on_ctrl_enter: sendOnCtrlEnter}
- };
+ this.state = {preReleaseFeatures: PreReleaseFeatures, settings, preReleaseFeaturesKeys, enabledFeatures};
}
updateSetting(setting, value) {
@@ -36,14 +49,45 @@ export default class AdvancedSettingsDisplay extends React.Component {
this.setState(settings);
}
- handleSubmit(setting) {
- const preference = PreferenceStore.setPreference(
- Constants.Preferences.CATEGORY_ADVANCED_SETTINGS,
- setting,
- this.state.settings[setting]
- );
+ toggleFeature(feature, checked) {
+ const settings = this.state.settings;
+ settings[Constants.FeatureTogglePrefix + feature] = String(checked);
+
+ let enabledFeatures = 0;
+ Object.keys(this.state.settings).forEach((setting) => {
+ if (setting.lastIndexOf(Constants.FeatureTogglePrefix) === 0 && this.state.settings[setting] === 'true') {
+ enabledFeatures++;
+ }
+ });
+
+ this.setState({settings, enabledFeatures});
+ }
+
+ saveEnabledFeatures() {
+ const features = [];
+ Object.keys(this.state.settings).forEach((setting) => {
+ if (setting.lastIndexOf(Constants.FeatureTogglePrefix) === 0) {
+ features.push(setting);
+ }
+ });
+
+ this.handleSubmit(features);
+ }
- Client.savePreferences([preference],
+ handleSubmit(settings) {
+ const preferences = [];
+
+ (Array.isArray(settings) ? settings : [settings]).forEach((setting) => {
+ preferences.push(
+ PreferenceStore.setPreference(
+ Constants.Preferences.CATEGORY_ADVANCED_SETTINGS,
+ setting,
+ String(this.state.settings[setting])
+ )
+ );
+ });
+
+ Client.savePreferences(preferences,
() => {
PreferenceStore.emitChange();
this.updateSection('');
@@ -118,6 +162,66 @@ export default class AdvancedSettingsDisplay extends React.Component {
);
}
+ let previewFeaturesSection;
+ let previewFeaturesSectionDivider;
+ if (this.state.preReleaseFeaturesKeys.length > 0) {
+ previewFeaturesSectionDivider = (
+ <div className='divider-light'/>
+ );
+
+ if (this.props.activeSection === 'advancedPreviewFeatures') {
+ const inputs = [];
+
+ this.state.preReleaseFeaturesKeys.forEach((key) => {
+ const feature = this.state.preReleaseFeatures[key];
+ inputs.push(
+ <div key={'advancedPreviewFeatures_' + feature.label}>
+ <div className='checkbox'>
+ <label>
+ <input
+ type='checkbox'
+ checked={this.state.settings[Constants.FeatureTogglePrefix + feature.label] === 'true'}
+ onChange={(e) => {
+ this.toggleFeature(feature.label, e.target.checked);
+ }}
+ />
+ {feature.description}
+ </label>
+ </div>
+ </div>
+ );
+ });
+
+ inputs.push(
+ <div key='advancedPreviewFeatures_helptext'>
+ <br/>
+ {'Check any pre-released features you\'d like to preview.'}
+ </div>
+ );
+
+ previewFeaturesSection = (
+ <SettingItemMax
+ title='Preview pre-release features'
+ inputs={inputs}
+ submit={this.saveEnabledFeatures}
+ server_error={serverError}
+ updateSection={(e) => {
+ this.updateSection('');
+ e.preventDefault();
+ }}
+ />
+ );
+ } else {
+ previewFeaturesSection = (
+ <SettingItemMin
+ title='Preview pre-release features'
+ describe={this.state.enabledFeatures + (this.state.enabledFeatures === 1 ? ' Feature ' : ' Features ') + 'enabled'}
+ updateSection={() => this.props.updateSection('advancedPreviewFeatures')}
+ />
+ );
+ }
+ }
+
return (
<div>
<div className='modal-header'>
@@ -145,6 +249,8 @@ export default class AdvancedSettingsDisplay extends React.Component {
<h3 className='tab-header'>{'Advanced Settings'}</h3>
<div className='divider-dark first'/>
{ctrlSendSection}
+ {previewFeaturesSectionDivider}
+ {previewFeaturesSection}
<div className='divider-dark'/>
</div>
</div>
diff --git a/web/react/stores/post_store.jsx b/web/react/stores/post_store.jsx
index 24b0d0dd0..a8f0f9c63 100644
--- a/web/react/stores/post_store.jsx
+++ b/web/react/stores/post_store.jsx
@@ -39,6 +39,7 @@ class PostStoreClass extends EventEmitter {
this.makePostsInfo = this.makePostsInfo.bind(this);
+ this.getPost = this.getPost.bind(this);
this.getAllPosts = this.getAllPosts.bind(this);
this.getEarliestPost = this.getEarliestPost.bind(this);
this.getLatestPost = this.getLatestPost.bind(this);
@@ -160,6 +161,17 @@ class PostStoreClass extends EventEmitter {
}
}
+ getPost(channelId, postId) {
+ const posts = this.postsInfo[channelId].postList;
+ let post = null;
+
+ if (posts.posts.hasOwnProperty(postId)) {
+ post = Object.assign({}, posts.posts[postId]);
+ }
+
+ return post;
+ }
+
getAllPosts(id) {
if (this.postsInfo.hasOwnProperty(id)) {
return Object.assign({}, this.postsInfo[id].postList);
@@ -554,7 +566,7 @@ class PostStoreClass extends EventEmitter {
return 0;
}
getCommentCount(post) {
- const posts = this.getPosts(post.channel_id).posts;
+ const posts = this.getAllPosts(post.channel_id).posts;
let commentCount = 0;
for (const id in posts) {
diff --git a/web/react/utils/constants.jsx b/web/react/utils/constants.jsx
index 372e15556..2009e07dd 100644
--- a/web/react/utils/constants.jsx
+++ b/web/react/utils/constants.jsx
@@ -398,5 +398,12 @@ export default {
},
NotificationPrefs: {
MENTION: 'mention'
+ },
+ FeatureTogglePrefix: 'feature_enabled_',
+ PRE_RELEASE_FEATURES: {
+ MARKDOWN_PREVIEW: {
+ label: 'markdown_preview', // github issue: https://github.com/mattermost/platform/pull/1389
+ description: 'Show markdown preview option in message input box'
+ }
}
};
diff --git a/web/react/utils/markdown.jsx b/web/react/utils/markdown.jsx
index b0ec64bfd..9d9bdfb7a 100644
--- a/web/react/utils/markdown.jsx
+++ b/web/react/utils/markdown.jsx
@@ -361,78 +361,78 @@ class MattermostLexer extends marked.Lexer {
// list
cap = this.rules.list.exec(src);
if (cap) {
+ src = src.substring(cap[0].length);
const bull = cap[2];
- let l = cap[0].length;
+
+ this.tokens.push({
+ type: 'list_start',
+ ordered: bull.length > 1
+ });
// 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'), '');
- }
+ let next = false;
+ const l = cap.length;
+ let i = 0;
+
+ for (; 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 the next list item belongs here.
+ // Backpedal if it does not belong in this list.
+ if (this.options.smartLists && i !== l - 1) {
+ const b = this.rules.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;
- }
+ // 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'
+ type: loose ?
+ 'loose_item_start' :
+ 'list_item_start'
});
- continue;
+ // Recurse.
+ this.token(item, false, bq);
+
+ this.tokens.push({
+ type: 'list_item_end'
+ });
}
+
+ this.tokens.push({
+ type: 'list_end'
+ });
+
+ continue;
}
// html
diff --git a/web/react/utils/utils.jsx b/web/react/utils/utils.jsx
index 9b2f7e057..80c377d7f 100644
--- a/web/react/utils/utils.jsx
+++ b/web/react/utils/utils.jsx
@@ -1221,3 +1221,7 @@ export function getPostTerm(post) {
return postTerm;
}
+
+export function isFeatureEnabled(feature) {
+ return PreferenceStore.getPreference(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, Constants.FeatureTogglePrefix + feature.label, {value: 'false'}).value === 'true';
+}
diff --git a/web/sass-files/sass/partials/_base.scss b/web/sass-files/sass/partials/_base.scss
index 2fc63443e..7efe70cb4 100644
--- a/web/sass-files/sass/partials/_base.scss
+++ b/web/sass-files/sass/partials/_base.scss
@@ -116,7 +116,7 @@ a:focus, a:hover {
.btn {
&.btn-danger {
color: #fff;
- &:hover, &:active {
+ &:hover, &:active, &:focus {
color: #fff;
}
}
diff --git a/web/sass-files/sass/partials/_post.scss b/web/sass-files/sass/partials/_post.scss
index b7609bb7d..b7a305427 100644
--- a/web/sass-files/sass/partials/_post.scss
+++ b/web/sass-files/sass/partials/_post.scss
@@ -53,6 +53,7 @@ body.ios {
top: 0;
left: 0;
box-shadow: none;
+ white-space: normal;
}
.textbox-preview-link, .textbox-help-link {
position: absolute;
@@ -283,14 +284,14 @@ body.ios {
.custom-textarea {
padding-top: 8px;
padding-right: 28px;
- max-height: 160px;
+ max-height: 162px !important;
overflow: auto;
line-height: 1.5;
}
.textarea-div {
padding-top: 8px;
padding-right: 30px;
- max-height: 160px;
+ max-height: 163px !important;
overflow: auto;
line-height: 1.5;
}
@@ -373,9 +374,9 @@ body.ios {
ul {
margin: 0;
padding: 0;
- list-style: none;
}
+
p {
margin: 0 0 1em;
line-height: 1.6em;
@@ -602,6 +603,11 @@ body.ios {
padding: 5px 0 0 20px;
}
+ ul, ol {
+ li ul, li ol {
+ padding: 0 0 0 20px
+ }
+ }
}
.post__link {