diff options
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 { |